logging4hackers 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/Gemfile +18 -0
- data/LICENCE +21 -0
- data/README.md +146 -0
- data/bin/logs_listen.rb +65 -0
- data/lib/logging.rb +7 -0
- data/lib/logging/code.rb +79 -0
- data/lib/logging/formatters.rb +168 -0
- data/lib/logging/io.rb +131 -0
- data/lib/logging/logger.rb +104 -0
- data/logger.png +0 -0
- data/logging4hackers.gemspec +38 -0
- data/spec/logging/code_spec.rb +64 -0
- data/spec/logging/formatters_spec.rb +135 -0
- data/spec/logging/io_spec.rb +41 -0
- data/spec/logging/logger_spec.rb +88 -0
- data/spec/spec_helper.rb +21 -0
- data/tasks.todo +21 -0
- data/test_app.rb +56 -0
- metadata +73 -0
data/CHANGELOG
ADDED
data/Gemfile
ADDED
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)
|
data/bin/logs_listen.rb
ADDED
@@ -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
data/lib/logging/code.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|