logging4hackers 0.1

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