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