logging4hackers 0.1

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.
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ = 0.0.1
2
+
3
+ * Initial release.
4
+ * IO modules: Raw, Null, File, Buffer, Pipe and AMQP.
5
+ * Formatters: Default, JustMessage and Colourful.
6
+ * Data syntax highlighting by converting data into JSON using CodeRay.
7
+ * CLI utility for simple log inspection.
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'coderay'
6
+
7
+ group(:test) do
8
+ gem 'rspec'
9
+ end
10
+
11
+ group(:documentation) do
12
+ gem 'yard'
13
+ gem 'redcarpet'
14
+ end
15
+
16
+ group(:release) do
17
+ gem 'changelog'
18
+ end
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 James C Russell aka botanicus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # About [![Travis CI](https://travis-ci.org/botanicus/logging4hackers.png)](https://travis-ci.org/botanicus/logging4hackers)
2
+
3
+ In the Ruby community it's very popular to **just append** to a file in `log/` directory in the current app. In many frameworks the developer **can't even change** the file. Damn it guys, **we can do better**!
4
+
5
+ ## Why Should I Care?
6
+
7
+ * You might want to have the log files in `/var/log` for simpler **log rotation**.
8
+ * You might not want to use files for logging at all. Not on the app server anyway. Especially considering that for **security reasons** it's better to send logs to a different server.
9
+ * You might want to **aggregate logs** from multiple servers.
10
+ * You might want to **filter logs** based on given pattern. Give me all error messages from all applications `logs.#.error`, all log items for database layer of testapp `logs.testapp.db.*`, all error messages for testapp `logs.testapp.*.error` etc.
11
+ * Isn't ssh & tail -f really, really, I mean **really** lame? With AMQP, just [subscribe to any pattern](#inspecting-remote-server) on any server you want from comfort of your own dev machine. Rock'n'roll!
12
+
13
+ ## Readable Logs (If You Want)
14
+
15
+ Besides, logs should be easy to read for the developers. That's why logging4hackers provides [colourful formatter](http://rubydoc.info/github/botanicus/logging4hackers/master/Logging/Formatters/Colourful) which uses colours instead of displaying log level as text and [Logger#inspect](http://rubydoc.info/github/botanicus/logging4hackers/master/Logging/Logger#inspect-instance_method) for showing objects as syntax-highlighted JSON.
16
+
17
+ <img src="https://raw.github.com/botanicus/logging4hackers/master/logger.png" />
18
+
19
+ ```ruby
20
+ require 'logging'
21
+ require 'logging/code'
22
+
23
+ logger = Logging::Logger.new do |logger|
24
+ logger.io = Logging::IO::Raw.new('testapp.logs.db')
25
+ logger.formatter = Logging::Formatters::Colourful.new
26
+ end
27
+
28
+ logger.info("Starting the app.")
29
+ logger.inspect({method: 'GET', path: '/ideas.json', response: '200'})
30
+ logger.warn("Now I'm a tad bored ...")
31
+ logger.error("OK, gotta sleep now.")
32
+ ```
33
+
34
+ *Note: Actually the screenshot shows how would you inspect messages published into RabbitMQ, whereas in the code I'm using the `IO::Raw` which only prints to console. [Example with AMQP](#logging-into-rabbitmq-local-or-remote) is longer.*
35
+
36
+ ## About [@botanicus](https://twitter.com/botanicus) ([blog](http://blog.101ideas.cz))
37
+
38
+ ![botanicus](http://www.gravatar.com/avatar/74c419a50563fa9e5044820c2697ffd6)
39
+ I'm a **launch-addict**, creating stuff that matters is my biggest passion. I **dropped out of high school** and learnt programming before I'd end up on a street. In just a few months I moved from <a title="Small town in mountains of Czech Republic">middle of nowhere</a> to **London** where I worked as a freelancer for companies like **VMware** on the **RabbitMQ team** for which I, <a title="Michael wasn't employed by VMware, he was hacking on AMQP in his free time. Kudos!">alongside</a> great hacker [michaelklishin](https://github.com/michaelklishin), rewrote the [AMQP gem](https://github.com/ruby-amqp/amqp).
40
+
41
+ I **contributed** to many famous OSS projects including **RubyGems**, **rSpec** and back in the g'd old days also to **Merb**. When EY decided to <a title="The so-called merge ... bunch of crap!">abandon Merb</a> I wrote my own web framework, [Rango](http://www.rubyinside.com/rango-ruby-web-app-framework-2858.html) (now <a title="These days my apps are API servers with heavy JS frontend.">discontinued</a>), the only framework in Ruby with [template inheritance](https://github.com/botanicus/template-inheritance).
42
+
43
+ My other hobbies include **travelling**, learning **languages** (你好!) and **personal development**. My [3 + 2 rule](http://lifehacker.com/5853732/take-a-more-realistic-approach-to-your-to+do-list-with-the-3-%252B-2-rule) was featured on LifeHacker.
44
+
45
+ My only goal for this year is to **launch a successful start-up**. Could [MatcherApp](http://www.matcherapp.com) be it?
46
+
47
+ # Use-Cases
48
+
49
+ ### Logging Into RabbitMQ (Local or Remote)
50
+
51
+ *TODO: Disconnect the AMQP, stop EM & terminate.*
52
+
53
+ * You can connect to RabbitMQ on **localhost** or **remote server**.
54
+ * So far it requires **some setup**. In the future I might **provide helpers** for this.
55
+ * It's the **most powerful** setup. You can **filter patterns**, you can **discard messages** just by **not subscribing** to those you're not interested in, you can **consume** given message **multiple times**, so you can for instance **duplicate logs** on two servers etc.
56
+ * Instead writing directly to AMQP you can **write to a named pipe** and have a **daemon** which **reroutes messages** to RabbitMQ as described [below](#clientserver-on-localhost-using-named-pipe).
57
+
58
+ ```ruby
59
+ require 'logging'
60
+ require 'logging/code'
61
+ require 'eventmachine'
62
+
63
+ EM.run do
64
+ require 'amq/client'
65
+
66
+ AMQ::Client.connect(adapter: 'eventmachine') do |connection|
67
+ channel = AMQ::Client::Channel.new(connection, 1)
68
+
69
+ channel.open
70
+
71
+ exchange = AMQ::Client::Exchange.new(connection, channel, 'amq.topic', :topic)
72
+
73
+ logger = Logging::Logger.new do |logger|
74
+ logger.io = Logging::IO::AMQP.new('testapp.logs.db', exchange)
75
+ logger.formatter = Logging::Formatters::Colourful.new
76
+ end
77
+
78
+ logger.info('Starting the app.')
79
+ logger.inspect({method: 'GET', path: '/ideas.json', response: '200'})
80
+ logger.error('Whops, no app defined, terminating.')
81
+ end
82
+ end
83
+ ```
84
+
85
+ ### Client/Server on Localhost using Named Pipe
86
+
87
+ * You **might not** want to **run EventMachine**.
88
+ * Setting up the Pipe logger on the client side requires **much less setup**, hence **much less stuff can wrong**.
89
+ * It's easy to write a daemon to **publish those messages** from the pipe **into RabbitMQ**. In the future I might provide one.
90
+
91
+ ```bash
92
+ # Create a named pipe.
93
+ mkfifo /tmp/loggingd.pipe
94
+
95
+ # Listen for messages coming to /tmp/loggingd.pipe.
96
+ tail -f /tmp/loggingd.pipe
97
+ ```
98
+
99
+ ```ruby
100
+ logger = Logging::Logger.new do |logger|
101
+ logger.io = Logging::IO::Pipe.new('testapp.logs.db', '/tmp/loggingd.pipe')
102
+ logger.formatter = Logging::Formatters::Colourful.new
103
+ end
104
+ ```
105
+
106
+ # Inspecting Remote Server
107
+
108
+ Often you want to figure out **what's going on on server**. This is how you do it:
109
+
110
+ ```bash
111
+ ./bin/logs_listen.rb 'logs.myapp.#' amqp://user:pass@remote_server/vhost
112
+ ```
113
+
114
+ It creates temporary queue which it binds to the `amq.topic` exchange which exists by default in any RabbitMQ installation. Then it binds the temporary queue to this exchange with pattern we provide (in this case it's `logs.myapp.#`). This makes sure all the subscribers gets all the messages they're interested in.
115
+
116
+ # Logging Best Practices
117
+
118
+ ### Don't Use Just One Logger Per App
119
+
120
+ In Ruby community people **often don't use loggers** at all. If they do, they work with **only one instance**. One logger instance for database, web server, application code and metrics. That **doesn't scale**.
121
+
122
+ If you use one logger instance per each module you can very easily **filter** based on **specific pattern**.
123
+
124
+ ```ruby
125
+ class DB
126
+ def self.logger
127
+ @logger ||= Logging::Logger.new do |logger|
128
+ logger.io = Logging::IO::Pipe.new('testapp.logs.db', '/tmp/loggingd.pipe')
129
+ logger.formatter = Logging::Formatters::Colourful.new
130
+ end
131
+ end
132
+ end
133
+
134
+ class App
135
+ def self.logger
136
+ @logger ||= Logging::Logger.new do |logger|
137
+ logger.io = Logging::IO::Pipe.new('testapp.app.db', '/tmp/loggingd.pipe')
138
+ logger.formatter = Logging::Formatters::Colourful.new
139
+ end
140
+ end
141
+ ```
142
+
143
+ # Links
144
+
145
+ * [YARD API Documentation](http://rubydoc.info/github/botanicus/logging4hackers/master)
146
+ * [Semantic Versioning](http://semver.org)
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'eventmachine'
5
+ require 'amq/client'
6
+
7
+ # Usage:
8
+ # ./bin/logs_listen.rb [routing key] [AMQP URI] [log dir]
9
+ #
10
+ # Routing Key Examples:
11
+ # myapp.logs.*.error
12
+ # myapp.logs.db.*
13
+ # myapp.logs.#
14
+ #
15
+ # AMQP URI:
16
+ # For instance amqp://user:pass@host/vhost
17
+ # See http://www.rabbitmq.com/uri-spec.html
18
+ #
19
+ # Log Directory (TBD):
20
+ # If the log directory is specified, messages will be saved into files.
21
+ # For example when specifying /var/log as my log directory, then when
22
+ # I'll get message with routing key myapp.logs.db.error, it will be
23
+ # written into /var/log/myapp.db.log.
24
+
25
+ # ARGV parsing.
26
+ routing_key = ARGV.first || '#.logs.#'
27
+ log_directory = ARGV[2]
28
+ amqp_opts = if ARGV[1]
29
+ puts "~ AMQP: #{AMQ::Client::Settings.parse_amqp_url(ARGV[1])}"
30
+ AMQ::Client::Settings.parse_amqp_url(ARGV[1]).merge(adapter: 'eventmachine')
31
+ else
32
+ {adapter: 'eventmachine'}
33
+ end
34
+
35
+ EM.run do
36
+ AMQ::Client.connect(amqp_opts) do |connection|
37
+
38
+ channel = AMQ::Client::Channel.new(connection, 1)
39
+ channel.open
40
+
41
+ queue = AMQ::Client::Queue.new(connection, channel)
42
+
43
+ queue.declare(false, false, false, true) do
44
+ puts "~ Creating autodeletable queue."
45
+ end
46
+
47
+ exchange = AMQ::Client::Exchange.new(connection, channel, 'amq.topic', :topic)
48
+
49
+ queue.bind(exchange.name, routing_key) do |frame|
50
+ puts "~ Binding it to the amq.topic exchange with routing key #{routing_key}."
51
+ end
52
+
53
+ queue.consume(true) do |consume_ok|
54
+ puts "~ Listening for messages ..."
55
+
56
+ queue.on_delivery do |basic_deliver, header, payload|
57
+ if log_directory
58
+ # basic_deliver.routing_key
59
+ else
60
+ puts payload
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/logging.rb ADDED
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ # This is the full package, including syntax highlighting
4
+ # for data using CodeRay and serialisation into JSON format.
5
+
6
+ require_relative 'logging/logger'
7
+ require_relative 'logging/code'
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+
3
+ require 'json'
4
+ require 'coderay'
5
+
6
+ module Logging
7
+ class Logger
8
+ # Inspect Ruby objects with syntax highlighting in JSON format.
9
+ #
10
+ # @overload inspect(*objects)
11
+ # @param objects [Array<#to_json>] List of objects for inspection.
12
+ #
13
+ # @overload inspect(label, *objects)
14
+ # @param label [String, Symbol] Label. For instance "Request time".
15
+ # @param objects [Array<#to_json>] List of objects for inspection.
16
+ #
17
+ # @example
18
+ # # Single object, no label.
19
+ # logger.inspect(path: "/", time: 0.0001)
20
+ #
21
+ # # Single object with String label.
22
+ # logger.inspect("Request data", path: "/", time: 0.0001)
23
+ #
24
+ # # Single object with Symbol label.
25
+ # logger.inspect(:request, {path: "/", time: 0.0001})
26
+ #
27
+ # # Multiple objects, no label.
28
+ # logger.inspect({path: "/", time: 0.001}, {path: "/test"})
29
+ #
30
+ # # Multiple objects with label.
31
+ # logger.inspect("Requests", {path: "/", time: 0.001}, {path: "/test"})
32
+ #
33
+ # @note
34
+ # This method is defined in {file:lib/logging/code.rb logging/code.rb}
35
+ # and requires {http://coderay.rubychan.de coderay}.
36
+ #
37
+ # @api public
38
+ def inspect(*objects)
39
+ label = ((objects.first.is_a?(String) ||
40
+ objects.first.is_a?(Symbol)) &&
41
+ objects.length > 1) ? objects.shift : nil
42
+
43
+ code = objects.map do |object|
44
+ begin
45
+ json = JSON.generate(object, object_nl: ' ', array_nl: ' ', space: ' ')
46
+ CodeRay.scan(json, :json).terminal
47
+ rescue
48
+ CodeRay.scan(object.inspect, :ruby).terminal
49
+ end
50
+ end.join("\n")
51
+
52
+ self.log(:inspect, label ? "#{label}: #{code}" : code)
53
+ end
54
+
55
+ # Measure how long does it take to execute provided block.
56
+ #
57
+ # @param label [#%] Formatting string.
58
+ # @param block [Proc] The block of which we'll measure the execution time.
59
+ #
60
+ # @example
61
+ # logger.measure_time("Request took %s") do
62
+ # sleep 0.1
63
+ # end
64
+ #
65
+ # @note
66
+ # This method is defined in {file:lib/logging/code.rb logging/code.rb}
67
+ # and requires {http://coderay.rubychan.de coderay}.
68
+ #
69
+ # @api public
70
+ def measure_time(label, &block)
71
+ before = Time.now.to_f; block.call
72
+ self.info(label % (Time.now.to_f - before))
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative 'formatters'
78
+
79
+ Logging::Formatters::Colourful::LEVELS[:inspect] = "\033[36m"
@@ -0,0 +1,168 @@
1
+ # encoding: utf-8
2
+
3
+ module Logging
4
+ # Formatters are used for formatting the log message.
5
+ # They can access level, label and the message.
6
+ # Their output is always string.
7
+ module Formatters
8
+
9
+ # Default formatter. No colours, just log level, time stamp,
10
+ # label and the actual log message.
11
+ class Default
12
+ # Format strings.
13
+ #
14
+ # The `single` key is used by {#format_single_message},
15
+ # whereas `header` is used by {#format_multiple_messages}.
16
+ FORMAT_STRINGS = {
17
+ single: '%-5s %s -- %s',
18
+ header: '%-5s %s'
19
+ }
20
+
21
+ # Format single log message.
22
+ #
23
+ # @param level [Symbol] Log level.
24
+ # @param label [String] Identifier, for instance logs.app.db.
25
+ # @param message [#to_s] The actual log message.
26
+ #
27
+ # @return [String] The log message.
28
+ #
29
+ # @see `FORMAT_STRINGS[:single]`
30
+ #
31
+ # @api plugin.
32
+ #
33
+ # @todo
34
+ # There's no documentation for the block yet, it's being
35
+ # used only for extending functionality from the subclasses.
36
+ # It should be probably refactored, otherwise it will get
37
+ # proper documentation.
38
+ def format_single_message(level, label, message, &block)
39
+ args = [label, timestamp, message]
40
+ args = block.call(*args) if block
41
+ sprintf(self.class::FORMAT_STRINGS[:single], *args)
42
+ end
43
+
44
+ # Format multiple log messages.
45
+ #
46
+ # @param level [Symbol] Log level.
47
+ # @param label [String] Identifier, for instance logs.app.db.
48
+ # @param messages [Array<#to_s>] The actual messages.
49
+ #
50
+ # @return [String] The log message.
51
+ #
52
+ # @see `FORMAT_STRINGS[:header]`
53
+ #
54
+ # @api plugin.
55
+ #
56
+ # @todo
57
+ # There's no documentation for the block yet, it's being
58
+ # used only for extending functionality from the subclasses.
59
+ # It should be probably refactored, otherwise it will get
60
+ # proper documentation.
61
+ def format_multiple_messages(level, label, messages, &block)
62
+ args = [level.to_s.upcase, timestamp]
63
+ args = block.call(*args) if block
64
+
65
+ header = sprintf(self.class::FORMAT_STRINGS[:header], *args)
66
+ messages.unshift(nil)
67
+ header + messages.join("\n ")
68
+ end
69
+
70
+ protected
71
+
72
+ # @api protected
73
+ def timestamp
74
+ Time.now.strftime('%H:%M:%S')
75
+ end
76
+ end
77
+
78
+ # This is useful for logging on console.
79
+ class JustMessage < Default
80
+ # The `single` key is used by {#format_single_message},
81
+ # whereas `header` is used by {#format_multiple_messages}.
82
+ FORMAT_STRINGS = {
83
+ single: '~ %s',
84
+ header: '~ %s'
85
+ }
86
+
87
+ # Format single log message.
88
+ #
89
+ # @param level [Symbol] Log level.
90
+ # @param label [String] Identifier, for instance logs.app.db.
91
+ # @param message [#to_s] The actual log message.
92
+ #
93
+ # @return [String] The log message.
94
+ #
95
+ # @see `FORMAT_STRINGS[:single]`
96
+ #
97
+ # @api plugin.
98
+ def format_single_message(level, label, message)
99
+ super(level, label, message) do |*args|
100
+ [args.last]
101
+ end
102
+ end
103
+
104
+ # Format multiple log messages.
105
+ #
106
+ # @param level [Symbol] Log level.
107
+ # @param label [String] Identifier, for instance logs.app.db.
108
+ # @param messages [Array<#to_s>] The actual messages.
109
+ #
110
+ # @return [String] The log message.
111
+ #
112
+ # @see `FORMAT_STRINGS[:header]`
113
+ #
114
+ # @api plugin.
115
+ def format_multiple_messages(level, label, messages)
116
+ super(level, label, messages) do |*args|
117
+ [args.last]
118
+ end
119
+ end
120
+ end
121
+
122
+ class Colourful < Default
123
+ # Colours for each log level.
124
+ LEVELS ||= {error: "\033[31m", warn: "\033[33m", info: "\033[36m"}
125
+
126
+ # The `single` key is used by {#format_single_message},
127
+ # whereas `header` is used by {#format_multiple_messages}.
128
+ FORMAT_STRINGS = {
129
+ single: "%s%-5s \033[37m%s\033[0m -- %s",
130
+ header: "%s%-5s \033[37m%s\033[0m"
131
+ }
132
+
133
+ # Format single log message.
134
+ #
135
+ # @param level [Symbol] Log level.
136
+ # @param label [String] Identifier, for instance logs.app.db.
137
+ # @param message [#to_s] The actual log message.
138
+ #
139
+ # @return [String] The log message.
140
+ #
141
+ # @see `FORMAT_STRINGS[:single]`
142
+ #
143
+ # @api plugin.
144
+ def format_single_message(level, label, message)
145
+ super(level, label, message) do |*args|
146
+ args.unshift(LEVELS[level])
147
+ end
148
+ end
149
+
150
+ # Format multiple log messages.
151
+ #
152
+ # @param level [Symbol] Log level.
153
+ # @param label [String] Identifier, for instance logs.app.db.
154
+ # @param messages [Array<#to_s>] The actual messages.
155
+ #
156
+ # @return [String] The log message.
157
+ #
158
+ # @see `FORMAT_STRINGS[:header]`
159
+ #
160
+ # @api plugin.
161
+ def format_multiple_messages(level, label, messages)
162
+ super(level, label, messages) do |*args|
163
+ args.unshift(LEVELS[level])
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
data/lib/logging/io.rb ADDED
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'formatters'
4
+
5
+ module Logging
6
+ # IO objects define how to write the log message.
7
+ # It can be on console, to a socket, to RabbitMQ,
8
+ # named pipe ... you name it!
9
+ module IO
10
+
11
+ # @abstract
12
+ # Subclass and override {#write}.
13
+ class Base
14
+ attr_reader :label
15
+
16
+ # @param label [String] Label. For instance `logs.app.db`.
17
+ def initialize(label)
18
+ @label = label
19
+ end
20
+
21
+ def write(message)
22
+ raise NotImplementedError.new
23
+ end
24
+ end
25
+
26
+ # Write on console.
27
+ class Raw < Base
28
+ def write(message)
29
+ puts message
30
+ end
31
+
32
+ def write_single_message(formatter, level, message)
33
+ self.write(formatter.format_single_message(level, self.label, message))
34
+ end
35
+
36
+ def write_multiple_messages(formatter, level, messages)
37
+ self.write(formatter.format_multiple_messages(level, self.label, messages))
38
+ end
39
+ end
40
+
41
+ # Discard everything.
42
+ class Null < Raw
43
+ def write(*)
44
+ end
45
+ end
46
+
47
+ # Write to a file.
48
+ class File < Raw
49
+ attr_reader :path
50
+ def initialize(label, path)
51
+ @label, @path = label, path
52
+ end
53
+
54
+ def file
55
+ @file ||= File.open(self.path, 'a')
56
+ end
57
+
58
+ def write(message)
59
+ file.puts(message)
60
+ end
61
+ end
62
+
63
+ # Write to a buffer.
64
+ class Buffer < Raw
65
+ def buffer
66
+ @buffer ||= Array.new
67
+ end
68
+
69
+ def log(level, *messages)
70
+ self.buffer << [level, messages]
71
+ end
72
+
73
+ def replay(io)
74
+ self.buffer.each do |level, *messages|
75
+ self.io.log(level, *messages)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Write to a named pipe.
81
+ class Pipe < Raw
82
+ attr_reader :path
83
+ def initialize(label, path)
84
+ @label, @path = label, path
85
+ end
86
+
87
+ def pipe
88
+ # The w+ means we don't block.
89
+ @pipe ||= open(self.path, 'w+')
90
+ end
91
+
92
+ def write(message)
93
+ self.pipe.puts(message)
94
+ self.pipe.flush
95
+ end
96
+ end
97
+
98
+ # Send message to an AMQP broker.
99
+ class AMQP < Base
100
+ def self.bootstrap(config)
101
+ require 'amq/client'
102
+
103
+ AMQ::Client.connect(config) do |connection|
104
+ channel = AMQ::Client::Channel.new(connection, 1)
105
+
106
+ channel.open
107
+
108
+ exchange = AMQ::Client::Exchange.new(connection, channel, 'amq.topic', :topic)
109
+
110
+ self.new(exchange)
111
+ end
112
+ end
113
+
114
+ def initialize(label, exchange)
115
+ @label, @exchange = label, exchange
116
+ end
117
+
118
+ def write_single_message(formatter, level, message)
119
+ self.write(formatter.format_single_message(level, self.label, message), "#{self.label}.#{level}")
120
+ end
121
+
122
+ def write_multiple_messages(formatter, level, messages)
123
+ self.write(formatter.format_multiple_messages(level, self.label, messages), "#{self.label}.#{level}")
124
+ end
125
+
126
+ def write(message, routing_key)
127
+ @exchange.publish(message, routing_key)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'io'
4
+ require_relative 'formatters'
5
+
6
+ module Logging
7
+ # Main class. Instantiate it to start logging.
8
+ # In reality all this class does is to provide
9
+ # convenience proxy to {file:lib/logging/io.rb io objects}
10
+ # and {file:lib/logging/formatters.rb formatters}.
11
+ class Logger
12
+ # Log levels. At the moment adding new log levels
13
+ # isn't supported. This might or might not change.
14
+ LEVELS ||= [:error, :warn, :info]
15
+
16
+ # Label is required only when you use the default
17
+ # {file:lib/logging/io.rb io object}.
18
+ attr_reader :label
19
+
20
+ # Create a new logger.
21
+ #
22
+ # @param label [String, nil] Label. For instance `logs.myapp.db`.
23
+ # @param block [Proc] Block with the newly created instance.
24
+ #
25
+ # @yield [logger] The logger instance which just has been created.
26
+ #
27
+ # @example
28
+ # # Create logger with default values, specifying
29
+ # # only the label (mandatory when not specifying io).
30
+ # logger = Logging::Logger.new('logs.my_app.db')
31
+ #
32
+ # # Create a logger specifying a custom formatter and io.
33
+ # logger = Logging::Logger.new('logs.my_app.db') do |logger|
34
+ # logger.io = Logging::IO::Pipe.new(logger.label, '/var/mypipe')
35
+ # logger.formatter = Logging::Formatters::Colourful.new
36
+ # end
37
+ def initialize(label = nil, &block)
38
+ @label = label
39
+ block.call(self) if block
40
+ end
41
+
42
+ # The cached io instance. If there's none, one will be created.
43
+ #
44
+ # @raise [RuntimeError] If the label isn't specified (when creating new deafult io).
45
+ def io
46
+ @io ||= begin
47
+ if self.label
48
+ IO::Raw.new(self.label)
49
+ else
50
+ raise "You have to provide label in Logger.new if you want to use the default io object!"
51
+ end
52
+ end
53
+ end
54
+
55
+ attr_writer :io
56
+
57
+ # The cached formatter instance. If there's none, one will be created.
58
+ def formatter
59
+ @formatter ||= Formatters::Default.new
60
+ end
61
+
62
+ attr_writer :formatter
63
+
64
+ # @!method error(*messages)
65
+ # Log an error message.
66
+ # @param messages [Array<#to_s>] Messages to be logged.
67
+ # @api public
68
+ #
69
+ # @!method warn(*messages)
70
+ # Log a warning message.
71
+ # @param messages [Array<#to_s>] Messages to be logged.
72
+ # @api public
73
+ #
74
+ # @!method info(*messages)
75
+ # Log an info message.
76
+ # @param messages [Array<#to_s>] Messages to be logged.
77
+ # @api public
78
+ LEVELS.each do |level|
79
+ define_method(level) do |*messages|
80
+ log(level, *messages)
81
+ end
82
+ end
83
+
84
+ # Underlaying function for logging any kind of message.
85
+ # The actual functionality is delegated to {#io}.
86
+ #
87
+ # @param level [Symbol] Log level.
88
+ # @param messages [Array<#to_s>] Messages to be logged.
89
+ def log(level, *messages)
90
+ if messages.length == 1
91
+ self.io.write_single_message(self.formatter, level, messages.first)
92
+ else
93
+ self.io.write_multiple_messages(self.formatter, level, messages)
94
+ end
95
+ end
96
+
97
+ # Delegate to `self.io#write.
98
+ #
99
+ # @param message [#to_s] Message to be written on the IO object.
100
+ def write(message)
101
+ self.io.write(message)
102
+ end
103
+ end
104
+ end
data/logger.png ADDED
Binary file
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env gem build
2
+ # encoding: utf-8
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'logging4hackers'
6
+ s.version = '0.1'
7
+ s.authors = ['James C Russell aka botanicus']
8
+ s.homepage = 'http://github.com/botanicus/logging4hackers'
9
+ s.summary = 'Logging using AMQP, pipes, sockets, ZeroMQ ... you name it!'
10
+ s.description = 'Any idiot can append to file. How about using AMQP? Pipes? ZeroMQ? Other cool shit??? Huh???'
11
+ s.email = 'james@101ideas.cz'
12
+
13
+ # Files.
14
+ ignore_patterns = ['Gemfile.lock', /\.gem$/, /^doc\//]
15
+
16
+ s.files = begin Dir.glob('**/*').
17
+ select { |path| File.file?(path) }.
18
+ delete_if do |file|
19
+ ignore_patterns.any? do |pattern|
20
+ file.match(pattern)
21
+ end
22
+ end
23
+ end
24
+
25
+ s.executables = Dir['bin/*'].map(&File.method(:basename))
26
+ s.require_paths = ['lib']
27
+
28
+ begin
29
+ require 'changelog'
30
+ rescue LoadError
31
+ warn "~ Please install the changelog gem to correctly set the post install message!\n\n"
32
+ else
33
+ s.post_install_message = CHANGELOG.new.version_changes
34
+ end
35
+
36
+ # RubyForge.
37
+ s.rubyforge_project = 'logging4hackers'
38
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Logging::Logger do
6
+ it "should add colour for inspect" do
7
+ Logging::Formatters::Colourful::LEVELS.should include(:inspect)
8
+ end
9
+
10
+ subject do
11
+ described_class.new('logs.my_app.db') do |logger|
12
+ logger.io = TestIO.new(logger.label)
13
+ logger.formatter = Logging::Formatters::JustMessage.new
14
+ end
15
+ end
16
+
17
+ def strip_escape_sequences(data)
18
+ data.gsub(/\e\[\d+(;\d+)?m/, '')
19
+ end
20
+
21
+ describe '#inspect' do
22
+ context 'with label' do
23
+ it "should use first string as the label if there's more than one argument" do
24
+ subject.inspect('Request data', path: '/')
25
+ message = strip_escape_sequences(subject.io.messages.last)
26
+ message.should eql('~ Request data: { "path": "/" }')
27
+ end
28
+
29
+ it "should use first symbol as the label if there's more than one argument" do
30
+ subject.inspect(:request, path: '/')
31
+ message = strip_escape_sequences(subject.io.messages.last)
32
+ message.should eql('~ request: { "path": "/" }')
33
+ end
34
+ end
35
+
36
+ context 'without label' do
37
+ it "should just inspect the string if it's the only argument" do
38
+ subject.inspect("Hello\nWorld!")
39
+ message = strip_escape_sequences(subject.io.messages.last)
40
+ message.should eql('~ "Hello\nWorld!"')
41
+ end
42
+
43
+ it "should inspect all arguments" do
44
+ subject.inspect({path: '/'}, {path: '/test'})
45
+
46
+ message = strip_escape_sequences(subject.io.messages.last)
47
+ expected = ['~ ', '{ "path": "/" }', "\n", '{ "path": "/test" }']
48
+
49
+ message.should eql(expected.join)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '#measure_time' do
55
+ # TODO: This is obviously wrong, but for the time being ...
56
+ it "should measure how long it takes to execute its block" do
57
+ subject.measure_time("Request took %s") do
58
+ sleep 0.055
59
+ end
60
+
61
+ subject.io.messages.last.should start_with("~ Request took 0.05")
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Logging::Formatters::Default do
6
+ describe '#format_single_message' do
7
+ let(:message) do
8
+ subject.format_single_message(:info, 'logs.test.app', "Hello World!")
9
+ end
10
+
11
+ it "should combine identifier with log level" do
12
+ pending "it's not including the log level yet" do
13
+ message.should match('logs.test.app.info')
14
+ end
15
+ end
16
+
17
+ it "should display timestamp" do
18
+ message.should match(/\d{2}:\d{2}:\d{2}/)
19
+ end
20
+
21
+ it "should display the actual message" do
22
+ message.should match("Hello World!")
23
+ end
24
+ end
25
+
26
+ describe '#format_multiple_messages' do
27
+ let(:message) do
28
+ subject.format_multiple_messages(:info, 'logs.test.app', ["Hello", "World!"])
29
+ end
30
+
31
+ it "should combine identifier with log level" do
32
+ pending "it's not including the log level yet" do
33
+ message.should match('logs.test.app.info')
34
+ end
35
+ end
36
+
37
+ it "should display timestamp" do
38
+ message.should match(/\d{2}:\d{2}:\d{2}/)
39
+ end
40
+
41
+ it "should display the actual message" do
42
+ message.should match("Hello\n World!")
43
+ end
44
+ end
45
+ end
46
+
47
+ describe Logging::Formatters::JustMessage do
48
+ describe '#format_single_message' do
49
+ let(:message) do
50
+ subject.format_single_message(:info, 'logs.test.app', "Hello World!")
51
+ end
52
+
53
+ it "should not display the identifier" do
54
+ message.should_not match('logs.test.app')
55
+ end
56
+
57
+ it "should not display the log level" do
58
+ message.should_not match('info')
59
+ end
60
+
61
+ it "should not display timestamp" do
62
+ message.should_not match(/\d{2}:\d{2}:\d{2}/)
63
+ end
64
+
65
+ it "should display the actual message" do
66
+ message.should eql("~ Hello World!")
67
+ end
68
+ end
69
+
70
+ describe '#format_multiple_messages' do
71
+ let(:message) do
72
+ subject.format_multiple_messages(:info, 'logs.test.app', ["Hello", "World!"])
73
+ end
74
+
75
+ it "should not display the identifier" do
76
+ message.should_not match('logs.test.app')
77
+ end
78
+
79
+ it "should not display the log level" do
80
+ message.should_not match('info')
81
+ end
82
+
83
+ it "should not display timestamp" do
84
+ pending "This obviously doesn't work now" do
85
+ message.should_not match(/\d{2}:\d{2}:\d{2}/)
86
+ end
87
+ end
88
+
89
+ it "should display the actual message" do
90
+ message.should match("Hello\n World!")
91
+ end
92
+ end
93
+ end
94
+
95
+ describe Logging::Formatters::Colourful do
96
+ describe '#format_single_message' do
97
+ let(:message) do
98
+ subject.format_single_message(:info, 'logs.test.app', "Hello World!")
99
+ end
100
+
101
+ it "should combine identifier with log level" do
102
+ pending "it's not including the log level yet" do
103
+ message.should match('logs.test.app.info')
104
+ end
105
+ end
106
+
107
+ it "should display timestamp" do
108
+ message.should match(/\d{2}:\d{2}:\d{2}/)
109
+ end
110
+
111
+ it "should display the actual message" do
112
+ message.should match("Hello World!")
113
+ end
114
+ end
115
+
116
+ describe '#format_multiple_messages' do
117
+ let(:message) do
118
+ subject.format_multiple_messages(:info, 'logs.test.app', ["Hello", "World!"])
119
+ end
120
+
121
+ it "should combine identifier with log level" do
122
+ pending "it's not including the log level yet" do
123
+ message.should match('logs.test.app.info')
124
+ end
125
+ end
126
+
127
+ it "should display timestamp" do
128
+ message.should match(/\d{2}:\d{2}:\d{2}/)
129
+ end
130
+
131
+ it "should display the actual message" do
132
+ message.should match("Hello\n World!")
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Logging::IO::Base do
6
+ subject { described_class.new('logs.app.tests') }
7
+
8
+ it "should have label" do
9
+ subject.label.should eql('logs.app.tests')
10
+ end
11
+ end
12
+
13
+ describe Logging::IO::Raw do
14
+ # TODO: Capture STDOUT and test.
15
+ end
16
+
17
+ describe Logging::IO::Null do
18
+ subject { described_class.new('logs.app.tests') }
19
+
20
+ it "should provide #write" do
21
+ expect {
22
+ subject.write("Hello World!")
23
+ }.not_to raise_error
24
+ end
25
+ end
26
+
27
+ describe Logging::IO::File do
28
+ # TODO
29
+ end
30
+
31
+ describe Logging::IO::Buffer do
32
+ # TODO
33
+ end
34
+
35
+ describe Logging::IO::Pipe do
36
+ # TODO
37
+ end
38
+
39
+ describe Logging::IO::AMQP do
40
+ # TODO
41
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Logging::Logger do
6
+ it "should define log levels" do
7
+ described_class::LEVELS.should include(:error)
8
+ described_class::LEVELS.should include(:warn)
9
+ described_class::LEVELS.should include(:info)
10
+ end
11
+
12
+ describe ".new" do
13
+ it "should create a new instance when no arguments are given" do
14
+ described_class.new.should be_kind_of(described_class)
15
+ end
16
+
17
+ it "should optionally take label" do
18
+ instance = described_class.new('logs.my_app.db')
19
+ instance.label.should eql('logs.my_app.db')
20
+ end
21
+
22
+ it "should create a new instance and yield the instance into a block" do
23
+ described_class.new { |logger| @instance = logger}
24
+ @instance.should be_kind_of(described_class)
25
+ end
26
+ end
27
+
28
+ context "instance methods" do
29
+ subject do
30
+ described_class.new('logs.my_app.db')
31
+ end
32
+
33
+ describe "#io" do
34
+ it "should have sensible default" do
35
+ subject.io.should be_kind_of(Logging::IO::Raw)
36
+ end
37
+
38
+ it "should fail if label isn't provided" do
39
+ expect { described_class.new.io }.to raise_error
40
+ end
41
+
42
+ it "should be writable" do
43
+ expect { subject.io = TestIO.new('label') }.not_to raise_error
44
+ end
45
+ end
46
+
47
+ describe "#formatter" do
48
+ it "should have sensible default" do
49
+ subject.formatter.should be_kind_of(Logging::Formatters::Default)
50
+ end
51
+
52
+ it "should be writable" do
53
+ formatter = Logging::Formatters::Colourful.new
54
+ expect { subject.formatter = formatter }.not_to raise_error
55
+ end
56
+ end
57
+
58
+ describe "logging methods" do
59
+ it "should define #info" do
60
+ expect { subject.info("Hello World!") }.not_to raise_error
61
+ end
62
+
63
+ it "should define #warn" do
64
+ expect { subject.warn("Hello World!") }.not_to raise_error
65
+ end
66
+
67
+ it "should define #error" do
68
+ expect { subject.error("Hello World!") }.not_to raise_error
69
+ end
70
+ end
71
+
72
+ describe "#log" do
73
+ it "should take log level and a single message"
74
+ it "should take log level and multiple messages"
75
+ end
76
+
77
+ describe "#write" do
78
+ before(:each) do
79
+ subject.io = TestIO.new('test')
80
+ end
81
+
82
+ it "should write to the io object" do
83
+ subject.io.write("Hello World!")
84
+ subject.io.messages.last.should eql("Hello World!")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'logging'
4
+
5
+ class TestIO < Logging::IO::Base
6
+ def write(message)
7
+ self.messages << message
8
+ end
9
+
10
+ def messages
11
+ @messages ||= Array.new
12
+ end
13
+
14
+ def write_single_message(formatter, level, message)
15
+ self.write(formatter.format_single_message(level, self.label, message))
16
+ end
17
+
18
+ def write_multiple_messages(formatter, level, messages)
19
+ self.write(formatter.format_multiple_messages(level, self.label, messages))
20
+ end
21
+ end
data/tasks.todo ADDED
@@ -0,0 +1,21 @@
1
+ Launch:
2
+ - README.
3
+
4
+ - Git tag first version (http://semver.org).
5
+ - Push the gem to RubyGems.org
6
+ - Announce on my blog.
7
+
8
+ Next Release:
9
+ - AMQP connection in bootstrap.
10
+ - CLI: Save log files if log directory provided.
11
+ - YARD & Markdown (I'm using `code` and it doesn't work) http://rubydoc.info/docs/yard/file/docs/GettingStarted.md#Configuring_YARD
12
+ - Is @api plugin in formatters what we really want?
13
+ - Finish specs & docs for io.
14
+ - Gemspec: changelog.
15
+ - Script for listening on pipe and populating RabbitMQ.
16
+ - Integration tests. Is it really writing into pipe? Into RabbitMQ? Test it!
17
+
18
+ Features for Future Versions:
19
+ - Sockets.
20
+ - Unix domain socket support (faster than sockets).
21
+ - ZeroMQ support.
data/test_app.rb ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require_relative 'lib/logging'
5
+ require_relative 'lib/logging/code'
6
+
7
+ # logger = Logging::Logger.new('testapp.logs.db')
8
+ #
9
+ # logger.info("Hello World!")
10
+ # logger.info("Just sayi' hi 'cause I'm gonna hang out here for a while")
11
+ # logger.info("No one up for a chat :/ ?")
12
+
13
+ =begin
14
+ logger = Logging::Logger.new do |logger|
15
+ logger.io = Logging::IO::Pipe.new('testapp.logs.db', '/tmp/loggingd.pipe')
16
+ logger.formatter = Logging::Formatters::Colourful.new
17
+ end
18
+
19
+ logger.info("App started")
20
+ logger.warn("I'm bored")
21
+ =end
22
+
23
+ __END__
24
+ require 'eventmachine'
25
+
26
+ EM.run do
27
+ require 'amq/client'
28
+
29
+ AMQ::Client.connect(adapter: 'eventmachine') do |connection|
30
+ channel = AMQ::Client::Channel.new(connection, 1)
31
+
32
+ channel.open
33
+
34
+ exchange = AMQ::Client::Exchange.new(connection, channel, 'amq.topic', :topic)
35
+
36
+ logger = Logging::Logger.new do |logger|
37
+ logger.io = Logging::IO::AMQP.new('testapp.logs.db', exchange)
38
+ logger.formatter = Logging::Formatters::Colourful.new
39
+ end
40
+
41
+ EM.add_periodic_timer(1) do
42
+ level = Logging::Logger::LEVELS[rand(Logging::Logger::LEVELS.length)]
43
+ logger.send(level, 'GET /ideas.json -- 20s')
44
+ logger.inspect({method: 'GET', path: '/ideas.json', response: '200'}.to_json)
45
+ logger.measure_time("Request took %s") do
46
+ sleep 0.23
47
+ end
48
+ logger.inspect({method: 'GET', path: '/ideas.json', response: '200'})
49
+ end
50
+
51
+ EM.add_periodic_timer(2.3) do
52
+ level = Logging::Logger::LEVELS[rand(Logging::Logger::LEVELS.length)]
53
+ logger.send(level, 'line 1', 'line 2', 'line 3')
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logging4hackers
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James C Russell aka botanicus
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Any idiot can append to file. How about using AMQP? Pipes? ZeroMQ? Other
15
+ cool shit??? Huh???
16
+ email: james@101ideas.cz
17
+ executables:
18
+ - logs_listen.rb
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - bin/logs_listen.rb
23
+ - CHANGELOG
24
+ - Gemfile
25
+ - lib/logging/code.rb
26
+ - lib/logging/formatters.rb
27
+ - lib/logging/io.rb
28
+ - lib/logging/logger.rb
29
+ - lib/logging.rb
30
+ - LICENCE
31
+ - logger.png
32
+ - logging4hackers.gemspec
33
+ - README.md
34
+ - spec/logging/code_spec.rb
35
+ - spec/logging/formatters_spec.rb
36
+ - spec/logging/io_spec.rb
37
+ - spec/logging/logger_spec.rb
38
+ - spec/spec_helper.rb
39
+ - tasks.todo
40
+ - test_app.rb
41
+ homepage: http://github.com/botanicus/logging4hackers
42
+ licenses: []
43
+ post_install_message: !binary |-
44
+ WxtbMzJtMC4wLjEbWzBtXSBJbml0aWFsIHJlbGVhc2UuClsbWzMybTAuMC4x
45
+ G1swbV0gSU8gbW9kdWxlczogUmF3LCBOdWxsLCBGaWxlLCBCdWZmZXIsIFBp
46
+ cGUgYW5kIEFNUVAuClsbWzMybTAuMC4xG1swbV0gRm9ybWF0dGVyczogRGVm
47
+ YXVsdCwgSnVzdE1lc3NhZ2UgYW5kIENvbG91cmZ1bC4KWxtbMzJtMC4wLjEb
48
+ WzBtXSBEYXRhIHN5bnRheCBoaWdobGlnaHRpbmcgYnkgY29udmVydGluZyBk
49
+ YXRhIGludG8gSlNPTiB1c2luZyBDb2RlUmF5LgpbG1szMm0wLjAuMRtbMG1d
50
+ IENMSSB1dGlsaXR5IGZvciBzaW1wbGUgbG9nIGluc3BlY3Rpb24uCg==
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project: logging4hackers
68
+ rubygems_version: 1.8.23
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Logging using AMQP, pipes, sockets, ZeroMQ ... you name it!
72
+ test_files: []
73
+ has_rdoc: