File Coverage

blib/lib/Pye/MongoDB.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Pye::MongoDB;
2              
3             # ABSTRACT: Log with Pye on top of MongoDB
4              
5 1     1   16668 use version;
  1         1403  
  1         5  
6              
7 1     1   50 use Carp;
  1         1  
  1         72  
8 1     1   211 use MongoDB;
  0            
  0            
9             use MongoDB::Code;
10             use Role::Tiny::With;
11             use Tie::IxHash;
12              
13             our $VERSION = "1.000001";
14             $VERSION = eval $VERSION;
15              
16             with 'Pye';
17              
18             our $NOW = MongoDB::Code->new(code => 'function() { return new Date() }');
19              
20             =head1 NAME
21              
22             Pye::MongoDB - Log with Pye on top of MongoDB
23              
24             =head1 SYNOPSIS
25              
26             use Pye::MongoDB;
27              
28             my $pye = Pye::MongoDB->new(
29             host => 'mongodb://logserver1:27017,logserver2:27017',
30             find_master => 1,
31             database => 'log_db',
32             collection => 'myapp_log'
33             );
34              
35             # now start logging
36             $pye->log($session_id, "Some log message", { data => 'example data' });
37              
38             # inspect the logs from the command line
39             pye -b MongoDB -d log_db -c myapp_log
40              
41             =head1 DESCRIPTION
42              
43             This package provides a MongoDB backend for the L<Pye> logging system. This is
44             currently the easiest backend to use, since no setup is needed in order to start
45             logging with it.
46              
47             Messages will be stored in a MongoDB database with the following keys:
48              
49             =over
50              
51             =item * C<session_id> - the session ID, a string, always exists
52              
53             =item * C<date> - the date the messages was logged, in ISODate format, always exists
54              
55             =item * C<text> - the text of the message, a string, always exists
56              
57             =item * C<data> - supplemental JSON structure, optional
58              
59             =back
60              
61             An index on the C<session_id> field will automatically be created.
62              
63             =head2 NOTES AND RECOMMENDATIONS
64              
65             As of this writing (MongoDB v2.6), MongoDB is kind of a storage guzzler. You might
66             find it useful to create a TTL index on the log collection. For example, the following
67             line (entered into the C<mongo> shell) will create a time-to-live index of 2 days
68             on a log collection:
69              
70             db.log_collection.ensureIndex({ date: 1 }, { expireAfterSeconds: 172800 })
71              
72             Alternatively, you could make the collection capped and limit it by size. Note, however,
73             that the _remove_session_logs() method will not work in that case.
74              
75             Also, consider using TokuMX as a drop-in replacement for MongoDB. It it faster, uses
76             much less storage space and supports ACID transactions.
77              
78             =head2 USING THE pye COMMAND LINE UTILITY
79              
80             The L<pye> command line utility, used to inspect logs, provides all command line options
81             to the L<new( [ %options ] )> constructor, which in turn passes anything to L<MongoDB::MongoClient>.
82             This means that if your database has replication, or requires authentication, you can
83             provide these options from the command line.
84              
85             For example:
86              
87             pye -b MongoDB
88             --host mongodb://server1:27017,server2:27017
89             --find_master=1
90             -d log_db
91             -c myapp_log
92             --username log_user
93             --password very_secret
94              
95             C<host>, C<find_master>, C<username> and C<password> in this example will be passed to C<MongoDB::MongoClient>.
96              
97             =head1 CONSTRUCTOR
98              
99             =head2 new( [ %options ] )
100              
101             Create a new instance of this class. All options are optional.
102              
103             =over
104              
105             =item * database - the name of the database, defaults to "logs"
106              
107             =item * collection (or table) - the name of the collection, defaults to "logs"
108              
109             =item * be_safe - whether to enable the C<safe> flag when inserting log messages,
110             defaults to a false value
111              
112             =back
113              
114             Any other option you provide will be passed to L<MongoDB::MongoClient>, so pass anything
115             needed in order to connect to the database server (such as C<host>, C<find_master>, etc.).
116              
117             =cut
118              
119             sub new {
120             my ($class, %opts) = @_;
121              
122             my $db_name = delete($opts{database}) || 'logs';
123             my $coll_name = delete($opts{collection}) || delete($opts{table}) || 'logs';
124             my $safety = delete($opts{be_safe}) || 0;
125              
126             # use the appropriate mongodb connection class
127             # depending on the version of the MongoDB driver
128             # installed
129             my $conn = version->parse($MongoDB::VERSION) >= v0.502.0 ?
130             MongoDB::MongoClient->new(%opts) :
131             MongoDB::Connection->new(%opts);
132              
133             my $db = $conn->get_database($db_name);
134             my $coll = $db->get_collection($coll_name);
135              
136             $coll->ensure_index({ session_id => 1 });
137              
138             return bless {
139             db => $db,
140             coll => $coll,
141             safety => $safety
142             }, $class;
143             }
144              
145             =head1 OBJECT METHODS
146              
147             The following methods implement the L<Pye> role, so you should refer to C<Pye>
148             for their documentation. Some methods, however, have some MongoDB-specific notes,
149             so keep reading.
150              
151             =head2 log( $session_id, $text, [ \%data ] )
152              
153             If C<\%data> is provided, this module will traverse it recursively, replacing any
154             hash-key that contains dots with semicolons, as MongoDB does not support dots in
155             field names.
156              
157             =cut
158              
159             sub log {
160             my ($self, $sid, $text, $data) = @_;
161              
162             my $date = $self->{db}->eval($NOW);
163              
164             my $doc = Tie::IxHash->new(
165             session_id => "$sid",
166             date => $date,
167             text => $text,
168             );
169              
170             if ($data) {
171             # make sure there are no dots in any hash keys,
172             # as mongodb cannot accept this
173             $doc->Push(data => $self->_remove_dots($data));
174             }
175              
176             $self->{coll}->insert($doc, { safe => $self->{safety} });
177             }
178              
179             =head2 session_log( $session_id )
180              
181             =cut
182              
183             sub session_log {
184             my ($self, $session_id) = @_;
185              
186             my $_map = sub {
187             my $d = shift;
188              
189             my $doc = {
190             session_id => $d->{session_id},
191             date => $d->{date}->ymd,
192             time => $d->{date}->hms.'.'.$d->{date}->millisecond,
193             text => $d->{text}
194             };
195             $doc->{data} = $d->{data} if $d->{data};
196             return $doc;
197             };
198              
199             local $MongoDB::Cursor::slave_okay = 1;
200              
201             map($_map->($_), $self->{coll}->find({ session_id => "$session_id" })->sort({ date => 1 })->all);
202             }
203              
204             =head2 list_sessions( [ \%opts ] )
205              
206             Takes all options defined by L<Pye>. The C<sort> option, however, takes a MongoDB
207             sorting definition, that is a hash-ref, e.g. C<< { _id => 1 } >>. This will
208             default to C<< { date => -1 } >>.
209              
210             =cut
211              
212             sub list_sessions {
213             my ($self, $opts) = @_;
214              
215             local $MongoDB::Cursor::slave_okay = 1;
216              
217             $opts ||= {};
218             $opts->{skip} ||= 0;
219             $opts->{limit} ||= 10;
220             $opts->{sort} ||= { date => -1 };
221              
222             map +{
223             id => $_->{_id},
224             date => $_->{date}->ymd,
225             time => $_->{date}->hms.'.'.$_->{date}->millisecond
226             }, @{$self->{coll}->aggregate([
227             { '$group' => { _id => '$session_id', date => { '$min' => '$date' } } },
228             { '$sort' => $opts->{sort} },
229             { '$skip' => $opts->{skip} },
230             { '$limit' => $opts->{limit} }
231             ])};
232             }
233              
234             ###################################
235             # _remove_dots( \%data ) #
236             #=================================#
237             # replaces dots in the hash-ref's #
238             # keys with semicolons, so that #
239             # mongodb won't complain about it #
240             ###################################
241              
242             sub _remove_dots {
243             my ($self, $data) = @_;
244              
245             if (ref $data eq 'HASH') {
246             my %data;
247             foreach (keys %$data) {
248             my $new = $_;
249             $new =~ s/\./;/g;
250              
251             if (ref $data->{$_} && ref $data->{$_} eq 'HASH') {
252             $data{$new} = $self->_remove_dots($data->{$_});
253             } elsif (ref $data->{$_} && ref $data->{$_} eq 'ARRAY') {
254             $data{$new} = [];
255             foreach my $item (@{$data->{$_}}) {
256             push(@{$data{$new}}, $self->_remove_dots($item));
257             }
258             } else {
259             $data{$new} = $data->{$_};
260             }
261             }
262             return \%data;
263             } elsif (ref $data eq 'ARRAY') {
264             my @data;
265             foreach (@$data) {
266             push(@data, $self->_remove_dots($_));
267             }
268             return \@data;
269             } else {
270             return $data;
271             }
272             }
273              
274             #####################################
275             # _remove_session_logs($session_id) #
276             #===================================#
277             # removes all log messages for the #
278             # supplied session ID. #
279             #####################################
280              
281             sub _remove_session_logs {
282             my ($self, $session_id) = @_;
283              
284             $self->{coll}->remove({ session_id => "$session_id" }, { safe => $self->{safety} });
285             }
286              
287             =head1 CONFIGURATION AND ENVIRONMENT
288            
289             C<Pye::MongoDB> requires no configuration files or environment variables.
290              
291             =head1 DEPENDENCIES
292              
293             C<Pye::MongoDB> depends on the following CPAN modules:
294              
295             =over
296              
297             =item * L<version>
298              
299             =item * L<Carp>
300              
301             =item * L<MongoDB>
302              
303             =item * L<Pye>
304              
305             =item * L<Role::Tiny>
306              
307             =item * L<Tie::IxHash>
308              
309             =back
310              
311             =head1 BUGS AND LIMITATIONS
312              
313             Please report any bugs or feature requests to
314             C<bug-Pye-MongoDB@rt.cpan.org>, or through the web interface at
315             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Pye-MongoDB>.
316              
317             =head1 SUPPORT
318              
319             You can find documentation for this module with the perldoc command.
320              
321             perldoc Pye::MongoDB
322              
323             You can also look for information at:
324              
325             =over 4
326            
327             =item * RT: CPAN's request tracker
328            
329             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Pye-MongoDB>
330            
331             =item * AnnoCPAN: Annotated CPAN documentation
332            
333             L<http://annocpan.org/dist/Pye-MongoDB>
334            
335             =item * CPAN Ratings
336            
337             L<http://cpanratings.perl.org/d/Pye-MongoDB>
338            
339             =item * Search CPAN
340            
341             L<http://search.cpan.org/dist/Pye-MongoDB/>
342            
343             =back
344            
345             =head1 AUTHOR
346            
347             Ido Perlmuter <ido@ido50.net>
348            
349             =head1 LICENSE AND COPYRIGHT
350            
351             Copyright (c) 2015, Ido Perlmuter C<< ido@ido50.net >>.
352              
353             This module is free software; you can redistribute it and/or
354             modify it under the same terms as Perl itself, either version
355             5.8.1 or any later version. See L<perlartistic|perlartistic>
356             and L<perlgpl|perlgpl>.
357            
358             The full text of the license can be found in the
359             LICENSE file included with this module.
360            
361             =head1 DISCLAIMER OF WARRANTY
362            
363             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
364             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
365             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
366             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
367             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
368             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
369             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
370             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
371             NECESSARY SERVICING, REPAIR, OR CORRECTION.
372            
373             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
374             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
375             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
376             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
377             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
378             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
379             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
380             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
381             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
382             SUCH DAMAGES.
383              
384             =cut
385              
386             1;
387             __END__