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 +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 [](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
|
+

|
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:
|