logging-couchdb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 / 2009-07-16
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
data/README.rdoc ADDED
@@ -0,0 +1,49 @@
1
+ = Logging CouchDB
2
+ * by Tim Pease
3
+ * http://github.com/TwP/logging-couchdb/tree/master
4
+
5
+ == DESCRIPTION:
6
+
7
+ Provides a CouchDB appender to the Ruby Logging framework. This appender
8
+ enables log messages to be written to a CouchDB instance.
9
+
10
+ == SYNOPSIS:
11
+
12
+ Logging.plugin :couch_db
13
+
14
+ Logging.appenders.couch_db( "app_id", :uri => "http://localhost:5984", :db_name => "logging" )
15
+
16
+ == REQUIREMENTS:
17
+
18
+ logging
19
+ rest-client
20
+ json (or json_pure)
21
+
22
+ == INSTALL:
23
+
24
+ gem install logging-couchdb
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License)
29
+
30
+ Copyright (c) 2009
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining
33
+ a copy of this software and associated documentation files (the
34
+ 'Software'), to deal in the Software without restriction, including
35
+ without limitation the rights to use, copy, modify, merge, publish,
36
+ distribute, sublicense, and/or sell copies of the Software, and to
37
+ permit persons to whom the Software is furnished to do so, subject to
38
+ the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be
41
+ included in all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
44
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
45
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
46
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
47
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
48
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
49
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ begin
10
+ load 'tasks/setup.rb'
11
+ rescue LoadError
12
+ raise RuntimeError, '### please install the "bones" gem ###'
13
+ end
14
+ end
15
+
16
+ ensure_in_path 'lib'
17
+ require 'logging'
18
+ require 'logging/plugins/couch_db'
19
+
20
+ task :default => 'spec:specdoc'
21
+
22
+ PROJ.name = 'logging-couchdb'
23
+ PROJ.authors = 'Tim Pease'
24
+ PROJ.email = 'tim.pease@gmail.com'
25
+ PROJ.url = 'http://logging.rubyforge.org/logging-couchdb'
26
+ PROJ.version = '1.0.0'
27
+ PROJ.rubyforge.name = 'logging'
28
+ PROJ.exclude << 'logging-couchdb.gemspec'
29
+ PROJ.readme_file = 'README.rdoc'
30
+ PROJ.ignore_file = '.gitignore'
31
+ PROJ.rdoc.remote_dir = 'logging-couchdb'
32
+
33
+ PROJ.spec.opts << '--color'
34
+
35
+ PROJ.ann.email[:server] = 'smtp.gmail.com'
36
+ PROJ.ann.email[:port] = 587
37
+ PROJ.ann.email[:from] = 'Tim Pease'
38
+
39
+ depend_on 'logging'
40
+ depend_on 'rest-client'
41
+ depend_on 'rspec'
42
+
43
+ # EOF
@@ -0,0 +1,169 @@
1
+
2
+ require 'rest_client'
3
+ begin
4
+ require 'json/ext'
5
+ rescue LoadError
6
+ require 'json'
7
+ end
8
+
9
+
10
+ module Logging::Appenders
11
+
12
+ # Accessor / Factory for the CouchDB appender.
13
+ #
14
+ def couch_db( *args )
15
+ return ::Logging::Appenders::CouchDB if args.empty?
16
+ ::Logging::Appenders::CouchDB.new(*args)
17
+ end
18
+
19
+ # This class provides an Appender that sends log messages to a CouchDB
20
+ # instance.
21
+ #
22
+ class CouchDB < ::Logging::Appender
23
+ include ::Logging::Appenders::Buffering
24
+
25
+ # The string that uniquely identifies this application in the CouchDB
26
+ # messages.
27
+ #
28
+ attr_accessor :app_id
29
+
30
+ # call-seq:
31
+ # CouchDB.new( name, :uri => 'http://localhost:5984',
32
+ # :db_name => 'logging', :app_id => name )
33
+ #
34
+ # Creates a new CouchDB appender that will format log events and send
35
+ # them to the CouchDB specified by the <tt>:uri</tt> and the
36
+ # <tt>:db_name</tt>. Your applications should specify a unique
37
+ # <tt>:app_id</tt> so that metrics about your application can be easily
38
+ # generated. If an app_id is not given, then the appender <tt>name</tt>
39
+ # is used as the app_id.
40
+ #
41
+ # The CouchDB appender uses message buffering. Log events are saved in
42
+ # bulk to the CouchDB instance. You can specify the buffer size by
43
+ # setting <tt>:auto_flushing</tt> to the number of messages to buffer.
44
+ # Setting the auto_flushing to +true+ will cause messages to be
45
+ # immediately sent to the CouchDB instance.
46
+ #
47
+ # Communication with the CouchDB instance is asynchronous; calls to the
48
+ # appender sould return quickly. However, there will be some delay
49
+ # before the log events appear in the CouchDB instance. The
50
+ # communication thread must be woken up and scheduled in order for the
51
+ # events to be posted.
52
+ #
53
+ def initialize( name, opts = {} )
54
+ opts = opts.merge(:layout => ::Logging::Layouts::CouchDB.new)
55
+ super(name, opts)
56
+
57
+ # initialize the connection to the CouchDB instance
58
+ uri = opts.getopt(:uri, 'http://localhost:5984')
59
+ db_name = opts.getopt(:db_name, 'logging')
60
+ self.app_id = opts.getopt(:app_id, name)
61
+
62
+ @db_uri = uri + '/' + db_name + '/_bulk_docs'
63
+ configure_buffering(opts)
64
+ start_thread
65
+ end
66
+
67
+ # Close the appender and wait for the internal writer thread to finish.
68
+ #
69
+ def close( *args )
70
+ super
71
+ Thread.pass until @dispatcher.status == 'sleep' or !@dispatcher.status
72
+ @dispatcher.wakeup if @dispatcher.status
73
+ @dispatcher.join(60)
74
+ self
75
+ end
76
+
77
+ # Send all buffered log events to the CouchDB instance. If the messages
78
+ # cannot be saved the appender will be disabled, and all messages in the
79
+ # buffer and all subsequent messages will be lost.
80
+ #
81
+ def flush
82
+ return self if buffer.empty?
83
+ @dispatch = true
84
+ @dispatcher.wakeup if @dispatcher.status == 'sleep'
85
+ self
86
+ end
87
+
88
+
89
+ private
90
+
91
+ # Write the given _event_ to the CouchDB instance. The message is
92
+ # formatted into a Ruby Hash instance suitable for conversion into a
93
+ # CouchDB JSON document.
94
+ #
95
+ def write( event )
96
+ h =
97
+ if event.instance_of?(::Logging::LogEvent)
98
+ layout.format event
99
+ else
100
+ {
101
+ :timestamp => ::Logging::Layouts::CouchDB.timestamp,
102
+ :logger => 'Unknown',
103
+ :level => 0,
104
+ :message => event.to_s
105
+ }
106
+ end
107
+ h[:app_id] = @app_id
108
+
109
+ buffer << h
110
+ flush if buffer.length >= auto_flushing || immediate?(event)
111
+
112
+ self
113
+ end
114
+
115
+ # This method performs the actual HTTP POST to the CouchDB instance. All
116
+ # messages in the buffer are posted using the CouchDB bulk storage
117
+ # semantics.
118
+ #
119
+ def post_events
120
+ return if @dispatch_buffer.empty?
121
+
122
+ payload = {:docs => @dispatch_buffer}.to_json
123
+ RestClient.post(@db_uri, payload)
124
+ #JSON.parse(RestClient.post(@db_uri, payload))
125
+ self
126
+ rescue StandardError => err
127
+ self.level = :off
128
+ ::Logging.log_internal {"appender #{name.inspect} has been disabled"}
129
+ ::Logging.log_internal(-2) {err}
130
+ raise
131
+ ensure
132
+ @dispatch_buffer.clear
133
+ end
134
+
135
+ # Creats the dispatcher thread that reads from the buffer and writes log
136
+ # events to the CouchDB instance.
137
+ #
138
+ def start_thread
139
+ @dispatch_buffer = []
140
+ @dispatch = false
141
+
142
+ @dispatcher = Thread.new(self) {
143
+ loop {
144
+ sleep(60) unless @dispatch or closed?
145
+
146
+ if closed?
147
+ sync {
148
+ @dispatch = false
149
+ @dispatch_buffer.concat @buffer
150
+ @buffer.clear
151
+ }
152
+ post_events
153
+ break
154
+
155
+ else
156
+ sync {
157
+ @dispatch = false
158
+ @buffer, @dispatch_buffer = @dispatch_buffer, @buffer
159
+ }
160
+ post_events
161
+ end
162
+ } # loop
163
+ } # Thread.new
164
+ end
165
+
166
+ end # class CouchDB
167
+ end # module Logging::Appenders
168
+
169
+ # EOF
@@ -0,0 +1,140 @@
1
+
2
+ module Logging::Layouts
3
+
4
+ # Accessor / Factory for the CouchDB layout.
5
+ #
6
+ def couch_db( *args )
7
+ return ::Logging::Layouts::CouchDB if args.empty?
8
+ ::Logging::Layouts::CouchDB.new(*args)
9
+ end
10
+
11
+ # This layout will produce log output in JSON format. This is different
12
+ # than the standard "Parseable" layout in that the JSON is tailored
13
+ # specifically for the CouchDB appender. Differences are mainly in the
14
+ # timestamp format and available options. The LogEvent data is only
15
+ # formatted into JSON and not inspect format or the standard to_s format.
16
+ #
17
+ # The information about the log event can be configured when the layout is
18
+ # created. Any or all of the following labels can be set as the _items_ to
19
+ # log:
20
+ #
21
+ # 'logger' Used to output the name of the logger that generated the
22
+ # log event.
23
+ # 'timestamp' Used to output the timestamp of the log event.
24
+ # 'level' Used to output the level of the log event.
25
+ # 'message' Used to output the application supplied message
26
+ # associated with the log event in JSON format.
27
+ # 'file' Used to output the file name where the logging request
28
+ # was issued.
29
+ # 'line' Used to output the line number where the logging request
30
+ # was issued.
31
+ # 'method' Used to output the method name where the logging request
32
+ # was issued.
33
+ # 'pid' Used to output the process ID of the currently running
34
+ # program.
35
+ # 'thread_id' Used to output the object ID of the thread that generated
36
+ # the log event.
37
+ # 'thread' Used to output the name of the thread that generated the
38
+ # log event. Name can be specified using Thread.current[:name]
39
+ # notation. Output empty string if name not specified. This
40
+ # option helps to create more human readable output for
41
+ # multithread application logs.
42
+ #
43
+ # These items are supplied to the layout as an array of strings. The items
44
+ # 'file', 'line', and 'method' will only work if the Logger generating the
45
+ # events is configured to generate tracing information. If this is not the
46
+ # case these fields will always be empty.
47
+ #
48
+ class CouchDB < ::Logging::Layout
49
+
50
+ # :stopdoc:
51
+ # Arguments to sprintf keyed to directive letters
52
+ DIRECTIVE_TABLE = {
53
+ 'logger' => 'event.logger',
54
+ 'timestamp' => 'CouchDB.timestamp(event.time)',
55
+ 'level' => 'event.level',
56
+ 'message' => 'format_obj(event.data)',
57
+ 'file' => 'event.file',
58
+ 'line' => 'event.line',
59
+ 'method' => 'event.method',
60
+ 'pid' => 'Process.pid',
61
+ 'thread_id' => 'Thread.current.object_id',
62
+ 'thread' => 'Thread.current[:name]'
63
+ }
64
+ TIME_FMT = '%Y-%m-%dT%H:%M:%S.%%06dZ'
65
+
66
+ # call-seq:
67
+ # CouchDB.create_format_method( layout )
68
+ #
69
+ # This method will create the +format+ method in the given CouchDB
70
+ # _layout_ based on the configured items for the layout instance.
71
+ #
72
+ def self.create_format_method( layout )
73
+ code = "undef :format if method_defined? :format\n"
74
+ code << "def format( event )\n{"
75
+
76
+ code << layout.items.map {|name|
77
+ "#{name.to_sym.inspect} => #{CouchDB::DIRECTIVE_TABLE[name]}"
78
+ }.join(',')
79
+ code << "\n}\nend"
80
+
81
+ (class << layout; self end).class_eval(code, __FILE__, __LINE__)
82
+ end
83
+ # :startdoc:
84
+
85
+ # call-seq:
86
+ # CouchDB.timestamp
87
+ #
88
+ # Returns a timestamp that can be sorted in CouchDB.
89
+ #
90
+ def self.timestamp( time = nil )
91
+ utc = (time || Time.now).utc
92
+ utc.strftime(TIME_FMT) % utc.usec
93
+ end
94
+
95
+ # call-seq:
96
+ # CouchDB.new( opts )
97
+ #
98
+ # Creates a new CouchDB layout using the following options:
99
+ #
100
+ # :items => %w[timestamp level logger message]
101
+ #
102
+ def initialize( opts = {} )
103
+ super
104
+ self.items = opts.getopt(:items, %w[timestamp level logger message])
105
+ end
106
+
107
+ attr_reader :items
108
+
109
+ # call-seq:
110
+ # layout.items = %w[timestamp level logger message]
111
+ #
112
+ # Set the log event items that will be formatted by this layout. These
113
+ # items, and only these items, will appear in the log output.
114
+ #
115
+ def items=( ary )
116
+ @items = Array(ary).map {|name| name.to_s.downcase}
117
+ valid = DIRECTIVE_TABLE.keys
118
+ @items.each do |name|
119
+ raise ArgumentError, "unknown item - #{name.inspect}" unless valid.include? name
120
+ end
121
+ self.class.create_format_method(self)
122
+ end
123
+
124
+ # Take the given _value_ and convert it into a format suitable for use
125
+ # in a JSON object structure.
126
+ #
127
+ def format_obj( value )
128
+ case value
129
+ when nil, Numeric, String, Array, Hash; value
130
+ when Exception
131
+ ary = ["<#{obj.class.name}> #{obj.message}"]
132
+ ary.concat(obj.backtrace) unless obj.backtrace.nil?
133
+ ary
134
+ else super(value) end
135
+ end
136
+
137
+ end # class CouchDB
138
+ end # module Logging::Layouts
139
+
140
+ # EOF
@@ -0,0 +1,15 @@
1
+
2
+ root = File.expand_path(
3
+ File.join(File.dirname(__FILE__), '..'))
4
+
5
+ module Logging::Plugins
6
+ module CouchDB
7
+ def initialize_couch_db
8
+ end
9
+ end # module CouchDB
10
+ end # module Logging::Plugins
11
+
12
+ require File.join(root, 'couch_db_layout')
13
+ require File.join(root, 'couch_db_appender')
14
+
15
+ # EOF
@@ -0,0 +1,26 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Logging::Appenders::CouchDB do
5
+
6
+ it "converts a string from camel-case to underscore" do
7
+ LittlePlugger.underscore('FooBarBaz').should == 'foo_bar_baz'
8
+ LittlePlugger.underscore('CouchDB').should == 'couch_db'
9
+ LittlePlugger.underscore('FOOBar').should == 'foo_bar'
10
+ LittlePlugger.underscore('Foo::Bar::BazBuz').should == 'foo/bar/baz_buz'
11
+ end
12
+
13
+ it "generates a default plugin path" do
14
+ LittlePlugger.default_plugin_path(LittlePlugger).should == 'little_plugger/plugins'
15
+ LittlePlugger.default_plugin_path(Spec::Runner).should == 'spec/runner/plugins'
16
+ end
17
+
18
+ it "generates a default plugin module" do
19
+ LittlePlugger.default_plugin_module('little_plugger').should == LittlePlugger
20
+ lambda {LittlePlugger.default_plugin_module('little_plugger/plugins')}.
21
+ should raise_error(NameError, 'uninitialized constant LittlePlugger::Plugins')
22
+ LittlePlugger.default_plugin_module('spec/runner').should == Spec::Runner
23
+ end
24
+ end
25
+
26
+ # EOF
@@ -0,0 +1,23 @@
1
+
2
+ require 'rubygems'
3
+ require 'logging'
4
+
5
+ Logging.plugin.clear
6
+ Logging.plugin << :none
7
+ Logging.init
8
+
9
+ require File.expand_path(
10
+ File.join(File.dirname(__FILE__), %w[.. lib logging plugins couch_db]))
11
+
12
+ Spec::Runner.configure do |config|
13
+ # == Mock Framework
14
+ #
15
+ # RSpec uses it's own mocking framework by default. If you prefer to
16
+ # use mocha, flexmock or RR, uncomment the appropriate line:
17
+ #
18
+ # config.mock_with :mocha
19
+ # config.mock_with :flexmock
20
+ # config.mock_with :rr
21
+ end
22
+
23
+ # EOF
data/tasks/ann.rake ADDED
@@ -0,0 +1,80 @@
1
+
2
+ begin
3
+ require 'bones/smtp_tls'
4
+ rescue LoadError
5
+ require 'net/smtp'
6
+ end
7
+ require 'time'
8
+
9
+ namespace :ann do
10
+
11
+ # A prerequisites task that all other tasks depend upon
12
+ task :prereqs
13
+
14
+ file PROJ.ann.file do
15
+ ann = PROJ.ann
16
+ puts "Generating #{ann.file}"
17
+ File.open(ann.file,'w') do |fd|
18
+ fd.puts("#{PROJ.name} version #{PROJ.version}")
19
+ fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
20
+ fd.puts(" #{PROJ.url}") if PROJ.url.valid?
21
+ fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
22
+ fd.puts
23
+ fd.puts("== DESCRIPTION")
24
+ fd.puts
25
+ fd.puts(PROJ.description)
26
+ fd.puts
27
+ fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
28
+ fd.puts
29
+ ann.paragraphs.each do |p|
30
+ fd.puts "== #{p.upcase}"
31
+ fd.puts
32
+ fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
33
+ fd.puts
34
+ end
35
+ fd.puts ann.text if ann.text
36
+ end
37
+ end
38
+
39
+ desc "Create an announcement file"
40
+ task :announcement => ['ann:prereqs', PROJ.ann.file]
41
+
42
+ desc "Send an email announcement"
43
+ task :email => ['ann:prereqs', PROJ.ann.file] do
44
+ ann = PROJ.ann
45
+ from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
46
+ to = Array(ann.email[:to])
47
+
48
+ ### build a mail header for RFC 822
49
+ rfc822msg = "From: #{from}\n"
50
+ rfc822msg << "To: #{to.join(',')}\n"
51
+ rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
52
+ rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
53
+ rfc822msg << "\n"
54
+ rfc822msg << "Date: #{Time.new.rfc822}\n"
55
+ rfc822msg << "Message-Id: "
56
+ rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
57
+ rfc822msg << File.read(ann.file)
58
+
59
+ params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
60
+ ann.email[key]
61
+ end
62
+
63
+ params[3] = PROJ.email if params[3].nil?
64
+
65
+ if params[4].nil?
66
+ STDOUT.write "Please enter your e-mail password (#{params[3]}): "
67
+ params[4] = STDIN.gets.chomp
68
+ end
69
+
70
+ ### send email
71
+ Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
72
+ end
73
+ end # namespace :ann
74
+
75
+ desc 'Alias to ann:announcement'
76
+ task :ann => 'ann:announcement'
77
+
78
+ CLOBBER << PROJ.ann.file
79
+
80
+ # EOF
data/tasks/bones.rake ADDED
@@ -0,0 +1,20 @@
1
+
2
+ if HAVE_BONES
3
+
4
+ namespace :bones do
5
+
6
+ desc 'Show the PROJ open struct'
7
+ task :debug do |t|
8
+ atr = if t.application.top_level_tasks.length == 2
9
+ t.application.top_level_tasks.pop
10
+ end
11
+
12
+ if atr then Bones::Debug.show_attr(PROJ, atr)
13
+ else Bones::Debug.show PROJ end
14
+ end
15
+
16
+ end # namespace :bones
17
+
18
+ end # HAVE_BONES
19
+
20
+ # EOF