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