File Coverage

blib/lib/Pye/SQL.pm
Criterion Covered Total %
statement 61 66 92.4
branch 9 12 75.0
condition 7 27 25.9
subroutine 13 14 92.8
pod 4 4 100.0
total 94 123 76.4


line stmt bran cond sub pod time code
1             package Pye::SQL;
2              
3             # ABSTRACT: Log with Pye into MySQL, PostgreSQL or SQLite
4              
5 2     2   61171 use warnings;
  2         3  
  2         62  
6 2     2   9 use strict;
  2         2  
  2         55  
7              
8 2     2   9 use Carp;
  2         6  
  2         119  
9 2     2   1833 use DBI;
  2         13661  
  2         92  
10 2     2   877 use JSON::MaybeXS qw/JSON/;
  2         11429  
  2         95  
11 2     2   777 use Role::Tiny::With;
  2         7785  
  2         1379  
12              
13             our $VERSION = "1.000000";
14             $VERSION = eval $VERSION;
15              
16             with 'Pye';
17              
18             our %NOW = (
19             mysql => 'NOW(6)',
20             pgsql => 'NOW()',
21             sqlite => 'strftime("%Y-%m-%d %H:%M:%f")'
22             );
23              
24             =head1 NAME
25              
26             Pye::SQL - Log with Pye into MySQL, PostgreSQL or SQLite
27              
28             =head1 SYNOPSIS
29              
30             use Pye::SQL;
31              
32             my $pye = Pye::SQL->new(
33             db_type => 'mysql', # or 'pgsql' or 'sqlite'
34             database => 'my_log_database',
35             table => 'myapp_logs'
36             );
37              
38             # now start logging
39             $pye->log($session_id, "Some log message", { data => 'example data' });
40              
41             # inspect the logs from the command line
42             pye -b SQL -t mysql -d my_log_database -T myapp_logs
43              
44             =head1 DESCRIPTION
45              
46             This package provides a relational SQL backend for the L logging system.
47             It currently supports MySQL, PostgreSQL and SQLite.
48              
49             All of these database systems will require prior creation of the target database
50             and table.
51              
52             =head2 MySQL
53              
54             When creating a table for logs, use something like this:
55              
56             CREATE TABLE logs (
57             session_id VARCHAR(60) NOT NULL,
58             date DATETIME(6) NOT NULL,
59             text TEXT NOT NULL,
60             data TEXT
61             );
62              
63             CREATE INDEX logs_per_session ON logs (session_id);
64              
65             For the C and C columns, note that the data type definition is
66             purely a suggestions. Use your own judgment as to which data types to use, and
67             what lengths, according to your application.
68              
69             =head2 PostgreSQL
70              
71             It is recommended to use PostgreSQL version 9.3 and up, supporting JSON or JSONB
72             columns. When creating a table for logs, use something like this:
73              
74             CREATE TABLE logs (
75             session_id VARCHAR(60) NOT NULL,
76             date TIMESTAMP WITH TIME ZONE NOT NULL,
77             text TEXT NOT NULL,
78             data JSON
79             );
80              
81             CREATE INDEX ON logs (session_id);
82              
83             If using v9.4 or up, C might better be a C column. As with C,
84             use your own judgment for the data type and length of C and C,
85             according to your application.
86              
87             If you're planning on running your own queries on the C column, you will need to
88             create an index on it. Read PostgreSQL's online documentation on JSON data types for
89             more information.
90              
91             =head2 SQLite
92              
93             When using SQLite as a backend, create the following table structure:
94              
95             CREATE TABLE logs (
96             session_id TEXT NOT NULL,
97             date TEXT NOT NULL,
98             text TEXT NOT NULL,
99             data TEXT
100             );
101              
102             CREATE INDEX logs_per_session ON logs (session_id);
103              
104             Note that, as opposed to other database systems, SQLite will take the path to the
105             database file as the C parameter, instead of a database name. You can also
106             provide C<:memory:> for an in-memory database.
107              
108             =head1 CONSTRUCTOR
109              
110             =head2 new( %options )
111              
112             Create a new instance of this class. The following options are supported:
113              
114             =over
115              
116             =item * db_type - the type of database (C, C or C), required
117              
118             =item * database - the name of the database to connect to, defaults to "logs" (if using SQLite,
119             this will be the path to the database file)
120              
121             =item * table - the name of the table to log into, defaults to "logs"
122              
123             =back
124              
125             The following options are supported by MySQL and PostgreSQL:
126              
127             =over
128              
129             =item * hostname - the host of the database server, defaults to C<127.0.0.1>
130              
131             =item * port - the port of the database server, defaults to C<3306> for MySQL, C<5432> for PostgreSQL
132              
133             =back
134              
135             =cut
136              
137             sub new {
138 1     1 1 13 my ($class, %opts) = @_;
139              
140 1 50 33     6 croak "You must provide the database type (db_type), one of 'mysql' or 'pgsql'"
141             unless $opts{db_type} &&
142             _in($opts{db_type}, qw/mysql pgsql sqlite/);
143              
144 1         3 $opts{db_type} = lc($opts{db_type});
145              
146 1   50     5 return bless {
147             dbh => DBI->connect(
148             _build_dsn(\%opts),
149             $opts{username},
150             $opts{password},
151             {
152             AutoCommit => 1,
153             RaiseError => 1
154             }
155             ),
156             json => JSON->new->allow_blessed->convert_blessed,
157             db_type => $opts{db_type},
158             table => $opts{table} || 'logs'
159             }, $class;
160             }
161              
162             =head1 OBJECT METHODS
163              
164             The following methods implement the L role, so you should refer to C
165             for their documentation. Some methods, however, have some MongoDB-specific notes,
166             so keep reading.
167              
168             =head2 log( $session_id, $text, [ \%data ] )
169              
170             If C<\%data> is provided, it will be encoded to JSON before storing in the database.
171              
172             =cut
173              
174             sub log {
175 3     3 1 1003056 my ($self, $sid, $text, $data) = @_;
176              
177 3 100       65 $self->{dbh}->do(
178             "INSERT INTO $self->{table} VALUES (?, ".$NOW{$self->{db_type}}.', ?, ?)',
179             undef, "$sid", $text, $data ? $self->{json}->encode($data) : undef
180             );
181             }
182              
183             =head2 session_log( $session_id )
184              
185             =cut
186              
187             sub session_log {
188 1     1 1 733 my ($self, $session_id) = @_;
189              
190 1         9 my $sth = $self->{dbh}->prepare("SELECT date, text, data FROM $self->{table} WHERE session_id = ? ORDER BY date ASC");
191 1         98 $sth->execute("$session_id");
192              
193 1         29 my @msgs;
194 1         24 while (my $row = $sth->fetchrow_hashref) {
195 2         6 my ($d, $t) = $self->_format_datetime($row->{date});
196 2         3 $row->{date} = $d;
197 2         4 $row->{time} = $t;
198 2 100       23 $row->{data} = $self->{json}->decode($row->{data})
199             if $row->{data};
200 2         105 push(@msgs, $row);
201             }
202              
203 1         4 $sth->finish;
204              
205 1         11 return @msgs;
206             }
207              
208             =head2 list_sessions( [ \%opts ] )
209              
210             Takes all options defined by L. The C option, however, takes a standard
211             C clause definition, e.g. C. This will default to C.
212              
213             =cut
214              
215             sub list_sessions {
216 1     1 1 616 my ($self, $opts) = @_;
217              
218 1   50     7 $opts ||= {};
219 1   50     7 $opts->{skip} ||= 0;
220 1   50     8 $opts->{limit} ||= 10;
221 1   50     5 $opts->{sort} ||= 'date DESC';
222              
223 1         11 my $sth = $self->{dbh}->prepare("SELECT session_id AS id, MIN(date) AS date FROM $self->{table} GROUP BY id ORDER BY $opts->{sort} LIMIT $opts->{limit} OFFSET $opts->{skip}");
224 1         175 $sth->execute;
225              
226 1         2 my @sessions;
227 1         35 while (my $row = $sth->fetchrow_hashref) {
228 2         9 my ($d, $t) = $self->_format_datetime($row->{date});
229 2         4 $row->{date} = $d;
230 2         4 $row->{time} = $t;
231 2         44 push(@sessions, $row);
232             }
233              
234 1         6 $sth->finish;
235              
236 1         16 return @sessions;
237             }
238              
239             sub _format_datetime {
240 4     4   7 my ($self, $date) = @_;
241              
242 4         30 my ($d, $t) = split(/T|\s/, $date);
243 4         9 $t = substr($t, 0, 12);
244              
245 4         11 return ($d, $t);
246             }
247              
248             sub _remove_session_logs {
249 0     0   0 my ($self, $session_id) = @_;
250              
251 0         0 $self->{dbh}->do("DELETE FROM $self->{table} WHERE session_id = ?", undef, "$session_id");
252             }
253              
254             sub _build_dsn {
255 1     1   2 my $opts = shift;
256              
257 1 50       5 if ($opts->{db_type} eq 'mysql') {
    50          
258 0   0     0 'DBI:mysql:database='.
      0        
      0        
259             ($opts->{database} || 'logs').
260             ';host='.($opts->{hostname} || '127.0.0.1').
261             ';port='.($opts->{port} || 3306).
262             ';mysql_enable_utf8=1';
263             } elsif ($opts->{db_type} eq 'pgsql') {
264 0   0     0 'dbi:Pg:dbname='.
      0        
      0        
265             ($opts->{database} || 'logs').
266             ';host='.($opts->{hostname} || '127.0.0.1').
267             ';port='.($opts->{port} || 5432);
268             } else {
269             # sqlite
270 1   50     14 'dbi:SQLite:dbname='.($opts->{database} || 'logs.db');
271             }
272             }
273              
274             sub _in {
275 1     1   1 my $val = shift;
276              
277 1         2 foreach (@_) {
278 3 100       15 return 1 if $val eq $_;
279             }
280              
281 0           return;
282             }
283              
284             =head1 CONFIGURATION AND ENVIRONMENT
285            
286             C requires no configuration files or environment variables.
287              
288             =head1 DEPENDENCIES
289              
290             C depends on the following CPAN modules:
291              
292             =over
293              
294             =item * L
295              
296             =item * L
297              
298             =item * L
299              
300             =item * L
301              
302             =back
303              
304             You will also need the appropriate driver for your database:
305              
306             =over
307              
308             =item * L for MySQL
309              
310             =item * L for PostgreSQL
311              
312             =item * L for SQLite
313              
314             =back
315              
316             =head1 BUGS AND LIMITATIONS
317              
318             Please report any bugs or feature requests to
319             C, or through the web interface at
320             L.
321              
322             =head1 SUPPORT
323              
324             You can find documentation for this module with the perldoc command.
325              
326             perldoc Pye::SQL
327              
328             You can also look for information at:
329              
330             =over 4
331            
332             =item * RT: CPAN's request tracker
333            
334             L
335            
336             =item * AnnoCPAN: Annotated CPAN documentation
337            
338             L
339            
340             =item * CPAN Ratings
341            
342             L
343            
344             =item * Search CPAN
345            
346             L
347            
348             =back
349            
350             =head1 AUTHOR
351            
352             Ido Perlmuter
353            
354             =head1 LICENSE AND COPYRIGHT
355            
356             Copyright (c) 2015, Ido Perlmuter C<< ido@ido50.net >>.
357              
358             This module is free software; you can redistribute it and/or
359             modify it under the same terms as Perl itself, either version
360             5.8.1 or any later version. See L
361             and L.
362            
363             The full text of the license can be found in the
364             LICENSE file included with this module.
365            
366             =head1 DISCLAIMER OF WARRANTY
367            
368             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
369             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
370             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
371             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
372             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
373             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
374             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
375             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
376             NECESSARY SERVICING, REPAIR, OR CORRECTION.
377            
378             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
379             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
380             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
381             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
382             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
383             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
384             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
385             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
386             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
387             SUCH DAMAGES.
388              
389             =cut
390              
391             1;
392             __END__