File Coverage

blib/lib/Log/Log4perl/Appender/Raven.pm
Criterion Covered Total %
statement 104 111 93.6
branch 23 38 60.5
condition 13 22 59.0
subroutine 15 16 93.7
pod 0 2 0.0
total 155 189 82.0


line stmt bran cond sub pod time code
1             package Log::Log4perl::Appender::Raven;
2             $Log::Log4perl::Appender::Raven::VERSION = '0.005';
3 9     9   40884 use Moose;
  9         3321672  
  9         57  
4              
5 9     9   52371 use Carp;
  9         15  
  9         643  
6 9     9   5608 use Data::Dumper;
  9         42238  
  9         609  
7 9     9   68 use Digest::MD5;
  9         16  
  9         293  
8 9     9   5520 use Sentry::Raven;
  9         2302350  
  9         313  
9 9     9   889 use Log::Log4perl;
  9         35699  
  9         81  
10 9     9   391 use Devel::StackTrace;
  9         23  
  9         198  
11 9     9   11843 use Safe;
  9         285453  
  9         522  
12 9     9   6594 use Text::Template;
  9         25834  
  9         10940  
13              
14             ## Configuration
15             has 'sentry_dsn' => ( is => 'ro', isa => 'Maybe[Str]' );
16             has 'sentry_timeout' => ( is => 'ro' , isa => 'Int' ,required => 1 , default => 1 );
17             has 'sentry_culprit_template' => ( is => 'ro', isa => 'Str', required => 1 , default => '{$function}');
18             has 'infect_die' => ( is => 'ro' , isa => 'Bool', default => 0 );
19             # STATIC CONTEXT
20             has 'context' => ( is => 'ro' , isa => 'HashRef', default => sub{ {}; });
21             # STATIC TAGS. They will go in the global context.
22             has 'tags' => ( is => 'ro' ,isa => 'HashRef', default => sub{ {}; });
23             # Log4Perl MDC key to look for tags
24             has 'mdc_tags' => ( is => 'ro' , isa => 'Maybe[Str]' , default => 'sentry_tags' );
25             # Log4perl MDC key to look for extra
26             has 'mdc_extra' => ( is => 'ro', isa => 'Maybe[Str]' , default => 'sentry_extra' );
27             # Log4perl MDC key to look for user data.
28             has 'mdc_user' => ( is => 'ro' ,isa => 'Maybe[Str]' , default => 'sentry_user' );
29             # Log4perl MDC key to look for http data.
30             has 'mdc_http' => ( is => 'ro' , isa => 'Maybe[Str]' , default => 'sentry_http' );
31              
32             ## End of configuration
33              
34             # Operation objects
35             has 'raven' => ( is => 'ro', isa => 'Sentry::Raven', lazy_build => 1);
36             has 'culprit_text_template' => ( is => 'ro', isa => 'Text::Template' , lazy_build => 1);
37             has 'safe' => ( is => 'ro' , isa => 'Safe', lazy_build => 1);
38              
39              
40             my %L4P2SENTRY = ('ALL' => 'info',
41             'TRACE' => 'debug',
42             'DEBUG' => 'debug',
43             'INFO' => 'info',
44             'WARN' => 'warning',
45             'ERROR' => 'error',
46             'FATAL' => 'fatal');
47              
48             sub BUILD{
49 10     10 0 21 my ($self) = @_;
50 10 100       470 if( $self->infect_die() ){
51 1         146 warn q|INFECTING SIG __DIE__ with Log4perl trickery. Ideally you should not count on that.
52              
53             See perldoc Log::Log4perl::Appender::Raven, section 'CODE WIHTOUT LOG4PERL'
54              
55             |;
56              
57             # Infect die. This is based on http://log4perl.sourceforge.net/releases/Log-Log4perl/docs/html/Log/Log4perl/FAQ.html#73200
58             $SIG{__DIE__} = sub{
59              
60             ## Are we called from within log4perl at all.
61             {
62 11     11   11476 my $frame_up = 0;
  11         21  
63 11         101 while( my @caller = caller($frame_up++) ){
64 45 100       319 if( $caller[0] =~ /^Log::Log4perl/ ){
65 7         62 return;
66             }
67             }
68             }
69              
70              
71             ## warn "CALLING die Handler";
72 4         8 my $method = 'fatal';
73              
74 4         6 my $level_up = 1;
75              
76             # In an eval, nothing is fatal:
77 4 50       20 if( $^S ){
78 4         10 $method = 'error';
79             }
80              
81 4         27 my ($package, $filename, $line,
82             $subroutine, @discard ) = caller(0);
83             # warn "CALLER PACKAGE IS $package\n";
84             # warn "CALLER SUBROUTINE IS $subroutine";
85 4 100       21 if( $package =~ /^Carp/ ){
86             # One level up please. We dont want to make Carp the culprit.
87             # and we want to know which is the calling package (to get the logger).
88 1         6 ($package, @discard ) = caller(1);
89 1         1 $level_up++ ;
90             }
91              
92 4   50     45 my $logger = Log::Log4perl->get_logger($package || '');
93              
94             ## This will make sure the following error or
95             ## fatal level work as usual.
96 4         494 local $Log::Log4perl::caller_depth =
97             $Log::Log4perl::caller_depth + $level_up ;
98              
99 4         24 $logger->$method(@_);
100              
101 4 50       174 if( $method eq 'error' ){
102             # Do not die. This will be catched by the enclosing eval.
103 4         49 return undef;
104             }
105              
106             # Not in an eval, die for good.
107 0         0 die @_;
108 1         50 };
109             }
110             }
111              
112             sub _build_safe{
113             # We do not authorize anything.
114 6     6   64 return Safe->new();
115             }
116              
117             {
118             # The fallback culprint template will signal itself as such in sentry.
119             my $FALLBACK_CULPRIT_TEMPLATE = 'FALLBACK {$function}';
120             sub _build_culprit_text_template{
121 8     8   19 my ($self) = @_;
122 8         368 my $tmpl = Text::Template->new( TYPE => 'STRING',
123             SOURCE => $self->sentry_culprit_template(),
124             );
125 8 100       1109 unless( $tmpl->compile() ){
126 1         131 warn "Cannot compile template from '".$self->sentry_culprit_template()."' ERROR:".$Text::Template::ERROR.
127             " - Will fallback to hardcoded '".$FALLBACK_CULPRIT_TEMPLATE."'";
128 1         7 $tmpl = Text::Template->new( TYPE => 'STRING', SOURCE => $FALLBACK_CULPRIT_TEMPLATE);
129 1 50       92 $tmpl->compile() or die "Invalid fallback template ".$FALLBACK_CULPRIT_TEMPLATE;
130             }
131 8         1099 return $tmpl;
132             }
133             }
134              
135             sub _build_raven{
136 9     9   17 my ($self) = @_;
137              
138 9   33     345 my $dsn = $self->sentry_dsn || $ENV{SENTRY_DSN} || confess("No sentry_dsn config or SENTRY_DSN in ENV");
139              
140              
141 8         17 my %raven_context = %{$self->context()};
  8         291  
142 8         324 $raven_context{tags} = $self->tags();
143              
144 8         325 return Sentry::Raven->new( sentry_dsn => $dsn,
145             timeout => $self->sentry_timeout,
146             %raven_context
147             );
148             }
149              
150             sub log{
151 19     19 0 27017 my ($self, %params) = @_;
152              
153             ## Any logging within this method will be discarded.
154 19 50       122 if( Log::Log4perl::MDC->get(__PACKAGE__.'-reentrance') ){
155 0         0 return;
156             }
157 19         288 Log::Log4perl::MDC->put(__PACKAGE__.'-reentrance', 1);
158              
159             # use Data::Dumper;
160             # warn Dumper(\%params);
161              
162             # Look there to see what sentry expects:
163             # http://sentry.readthedocs.org/en/latest/developer/client/index.html#building-the-json-packet
164              
165 19 50       186 my $sentry_message = length($params{message}) > 1000 ? substr($params{message}, 0 , 1000) : $params{message};
166 19         36 my $sentry_logger = $params{log4p_category};
167 19   50     94 my $sentry_level = $L4P2SENTRY{$params{log4p_level}} || 'info';
168              
169             # We are 4 levels down after the standard Log4perl caller_depth
170 19         74 my $caller_offset = Log::Log4perl::caller_depth_offset( $Log::Log4perl::caller_depth + 4 );
171              
172             ## Stringify arguments NOW (no_refs => 1). This avoids sending huuuuuge objects when
173             ## Serializing this stack trace inside Sentry::Raven
174 19         352 my $caller_frames = Devel::StackTrace->new( no_refs => 1);
175             {
176             ## Remove the frames from the Log4Perl layer.
177 19         4959 my @frames = $caller_frames->frames();
  19         71  
178 19         6112 splice(@frames, 0, $caller_offset);
179 19         59 $caller_frames->frames(@frames);
180             }
181              
182 19         470 my $call_depth = $caller_offset;
183              
184             # Defaults caller properties
185 19         52 my $caller_properties = {
186             function => 'main',
187             };
188 19         143 my @log_call_info = caller($call_depth - 1);
189 19   50     111 $caller_properties->{line} = $log_call_info[2] || 'NOTOPLINE';
190              
191             {
192             # Go up the caller ladder until the first non eval
193 19         28 while( my @caller_info = caller($call_depth) ){
  19         129  
194              
195             # Skip evals and __ANON__ methods.
196             # The anon method will make that compatible with the new Log::Any (>0.15)
197 19   50     109 my $caller_string = $caller_info[3] || '';
198              
199 19 100 100     199 unless( ( $caller_string eq '(eval)' )
200             || ( scalar(reverse($caller_string)) =~ /^__NONA__/ )
201             # ^ This test for the caller string to end with __ANON__ , but faster.
202             ){
203             # This is good.
204             # Subroutine name, or filename, or just main
205 14   0     49 $caller_properties->{function} = $caller_info[3] || $caller_info[1] || 'main';
206             # For other properties, we are interested in the place where $log->something
207             # was called, not were the caller of $log->something was called from
208 14         79 my @log_call_info = caller($call_depth - 1);
209 14   50     47 $caller_properties->{line} = $log_call_info[2] || 'NOLINE';
210 14         50 last;
211             }
212 5         32 $call_depth++;
213             }
214             }
215              
216 19         38 my $tags = {};
217 19 50       908 if( my $mdc_tags = $self->mdc_tags() ){
218 19   100     71 $tags = Log::Log4perl::MDC->get($mdc_tags) || {};
219             }
220              
221 19         210 my $extra = {};
222 19 50       775 if( my $mdc_extra = $self->mdc_extra() ){
223 19   100     71 $extra = Log::Log4perl::MDC->get($mdc_extra) || {};
224             }
225              
226 19         190 my $user;
227 19 50       736 if( my $mdc_user = $self->mdc_user() ){
228 19         59 $user = Log::Log4perl::MDC->get($mdc_user);
229             }
230              
231 19         118 my $http;
232 19 50       732 if( my $mdc_http = $self->mdc_http() ){
233 19         68 $http = Log::Log4perl::MDC->get($mdc_http);
234             }
235              
236             # Calculate the culprit from the template
237             my $sentry_culprit = $self->culprit_text_template->fill_in(
238             SAFE => $self->safe(),
239             HASH => {
240 19         235 %{$caller_properties},
241             message => $sentry_message,
242             sign => sub{
243 0     0   0 my ($string, $offset, $length) = @_;
244 0 0       0 defined( $string ) || ( $string = '' );
245 0 0       0 defined( $offset ) || ( $offset = 0 );
246 0 0       0 defined( $length ) || ( $length = 4 );
247 0         0 return substr(Digest::MD5::md5_hex(substr($string, $offset, $length)), 0, 4);
248             }
249 19         893 });
250              
251             # OK WE HAVE THE BASIC Sentry options.
252             $self->raven->capture_message($sentry_message,
253             logger => $sentry_logger,
254             level => $sentry_level,
255             culprit => $sentry_culprit,
256             tags => $tags,
257             extra => $extra,
258             Sentry::Raven->stacktrace_context( $caller_frames ),
259             ( $user ? Sentry::Raven->user_context(%$user) : () ),
260 19 100       44380 ( $http ? Sentry::Raven->request_context( ( delete $http->{url} ) , %$http ) : () )
    100          
261             );
262              
263 19         9561 Log::Log4perl::MDC->put(__PACKAGE__.'-reentrance', undef);
264             }
265              
266              
267             __PACKAGE__->meta->make_immutable();
268              
269              
270             =head1 NAME
271              
272             Log::Log4perl::Appender::Raven - Append log events to your Sentry account.
273              
274             =head1 BUILD STATUS
275              
276             =begin html
277              
278             <a href="https://travis-ci.org/jeteve/l4p-appender-raven"><img src="https://travis-ci.org/jeteve/l4p-appender-raven.svg?branch=master"></a>
279              
280             =end html
281              
282             =head1 WARNING(s)
283              
284             This appender will send ALL the log events it receives to your
285             Sentry DSN synchronously. If you generate a lot of logging, that can make your sentry account
286             saturate quite quickly and your application come to a severe slowdown.
287              
288             Using Log4perl appender's Threshold or L<Log::Log4perl::Filter> in your log4perl config, and
289             experimenting a little bit is Highly Recommended.
290              
291             Remember sentry is designed to record errors, so hopefully your application will
292             not generate too many of them.
293              
294             You have been warned.
295              
296             =head1 SYNOPSIS
297              
298             Read the L<CONFIGURATION> section, then use Log4perl just as usual.
299              
300             If you are not familiar with Log::Log4perl, please check L<Log::Log4perl>
301              
302             In a nutshell, here's the minimul l4p config to output anything from ERROR to Sentry:
303              
304             log4perl.rootLogger=DEBUG, Raven
305              
306             log4perl.appender.Raven=Log::Log4perl::Appender::Raven
307             log4perl.appender.Raven.Threshold=ERROR
308             log4perl.appender.Raven.sentry_dsn="https://user:key@sentry-host.com/project_id"
309             log4perl.appender.Raven.layout=Log::Log4perl::Layout::PatternLayout
310             log4perl.appender.Raven.layout.ConversionPattern=%X{chunk} %d %F{1} %L> %m %n
311              
312              
313             =head1 CONFIGURATION
314              
315             This is just another L<Log::Log4perl::Appender>.
316              
317             =head2 Simple Configuration
318              
319             The only mandatory configuration key
320             is *sentry_dsn* which is your sentry dsn string obtained from your sentry account.
321             See http://www.getsentry.com/ and https://github.com/getsentry/sentry for more details.
322              
323             Alternatively to setting this configuration key, you can set an environment variable SENTRY_DSN
324             with the same setting. - Not recommended -
325              
326             Example:
327              
328             log4perl.rootLogger=ERROR, Raven
329              
330             layout_class=Log::Log4perl::Layout::PatternLayout
331             layout_pattern=%X{chunk} %d %F{1} %L> %m %n
332              
333             log4perl.appender.Raven=Log::Log4perl::Appender::Raven
334             log4perl.appender.Raven.sentry_dsn="http://user:key@host.com/project_id"
335             log4perl.appender.Raven.sentry_timeout=1
336             log4perl.appender.Raven.layout=${layout_class}
337             log4perl.appender.Raven.layout.ConversionPattern=${layout_pattern}
338              
339             =head2 Configuring the culprit string
340              
341             By default, this appender will calculate the Sentry culprit to be
342             the fully qualified name of the function that called the log method, as Sentry
343             recommends.
344              
345             If you require more flexibility and precision in your culprit, you can
346             configure it as a template. For instance:
347              
348             log4perl.appender.Raven.sentry_culprit_template={$function}-{$line}
349              
350             The default is '{$function}', as Sentry prescribes. But most people will probably
351             be more happy with the added {$line} element, as it makes discriminating between culprits easier.
352              
353             The template format follows L<Text::Template> and the available variables and functions are as follow:
354              
355             =over
356              
357             =item function
358              
359             The fully qualified name of the function that called the log method.
360              
361             =item line
362              
363             The line at which the log method was called
364              
365             =item message
366              
367             The Log4perl generated message. Keep in mind that this is the message AFTER it has been calculated by
368             the layout pattern.
369              
370             =item sign($string, $offset, $length)
371              
372             A function that calculates a small (4 chars) signature of the given string. $string, $offset
373             and $length are optional.
374              
375             This is useful for instance if some part of your code manage errors in a centralized way, or in other
376             terms if the place at which you call '$log->error()' can output various messages.
377             To help discriminating between culprit, you can for instance configure your culprit template:
378              
379              
380             log4perl.appender.Raven.sentry_culprit_template={$function}-{$line}-{sign($message, 30, 4)}
381              
382             Note that in the example, we look at a part of the message after the 30th character, which
383             helps skipping the common message parts defined by your message layout. Adjust this number (30)
384             to make sure you pick a substring of your message in a meaningful area.
385              
386             =back
387              
388             =head2 Timeout
389              
390             The default timeout is 1 second. Feel free to bump it up. If sending an event
391             timesout (or if the sentry host is down or doesn't exist), a plain Perl
392             warning will be output.
393              
394             =head2 Configuration with Static Tags
395              
396             You have the option of predefining a set of tags that will be send to
397             your Sentry installation with every event. Remember Sentry tags have a name
398             and a value (they are not just 'labels').
399              
400             Example:
401              
402             ...
403             log4perl.appender.Raven.tags.application=myproduct
404             log4perl.appender.Raven.tags.installation=live
405             ...
406              
407             =head2 Configure and use Dynamic Tagging
408              
409             Dynamic tagging is performed using the Log4Perl MDC mechanism.
410             See L<Log::Log4perl::MDC> if you are not familiar with it.
411              
412             Anywhere in your code.
413              
414             ...
415             Log::Log4perl::MDC->set('sentry_tags' , { subsystem => 'my_subsystem', ... });
416             $log->error("Something very wrong");
417             ...
418              
419             Or specify which key to capture in config:
420              
421             ...
422             log4perl.appender.Raven.mdc_tags=my_sentry_tags
423             ...
424              
425              
426             Note that tags added this way will be added to the statically define ones, or override them in case
427             of conflict.
428              
429             Note: Tags are meant to categorize your Sentry events and will be displayed
430             in the Sentry GUI like any other category.
431              
432             =head2 Configure and use User Data
433              
434             Sentry supports structured user data that can be added to your event.
435             User data works a bit like the tags, except only three keys are supported:
436              
437             id, username and email. See L<Sentry::Raven> (capture_user) for a description of those keys.
438              
439              
440             In your code:
441              
442             ...
443             Log::Log4perl::MDC->set('sentry_user' , { id => '123' , email => 'jeteve@cpan.org', username => 'jeteve' });
444             $log->error("Something very wrong");
445             ...
446              
447              
448             Or specify the MDC key to capture in Config:
449              
450             ...
451             log4perl.appender.Raven.mdc_user=my_sentry_user
452             ...
453              
454              
455             =head2 Configure and use HTTP Request data.
456              
457             Sentry support HTTP Request structured data that can be added to your event.
458             HTTP Data work a bit like tags, except only a number of keys are supported:
459              
460             url, method, data, query_string, cookies, headers, env
461              
462             See L<Sentry::Raven> (capture_request) or interface 'Http' in L<http://sentry.readthedocs.org/en/latest/developer/interfaces/index.html>
463             for a full description of those keys.
464              
465             In your code:
466              
467             ...
468             Log::Log4perl::MDC->set('sentry_http' , { url => 'http://www.example.com' , method => 'GET' , ... });
469             $log->error("Something very wrong");
470             ...
471              
472             Or specify the MDC key to capture in Config:
473              
474             ...
475             log4perl.appender.Raven.mdc_http=my_sentry_http
476             ...
477              
478             =head2 Configure and use Dynamic Extra
479              
480             Sentry allows you to specify any data (as a Single level HashRef) that will be stored with the Event.
481              
482             It's very similar to dynamic tags, except its not tags.
483              
484             Then anywere in your code:
485              
486             ...
487             Log::Log4perl::MDC->set('my_sentry_extra' , { session_id => ... , ... });
488             $log->error("Something very wrong");
489             ...
490              
491              
492             Or specify MDC key to capture in config:
493              
494             ...
495             log4perl.appender.Raven.mdc_extra=my_sentry_extra
496             ...
497              
498             =head2 Configuration with a Static Context.
499              
500             You can use lines like:
501              
502             log4perl.appender.Raven.context.platform=myproduct
503              
504             To define static L<Sentry::Raven> context. The list of context keys supported is not very
505             long, and most of them are defined dynamically when you use this package anyway.
506              
507             See L<Sentry::Raven> for more details.
508              
509             =head1 USING Log::Any
510              
511             This is tested to work with Log::Any just the same way it works when you use Log4perl directly.
512              
513             =head1 CODE WITHOUT LOG4PERL
514              
515             Warning: Experimental feature.
516              
517             If your code, or some of its dependencies is not using Log4perl, you might want
518             to consider infecting the __DIE__ pseudo signal with some amount of trickery to have die (and Carp::confess/croak)
519             calls go through log4perl.
520              
521             This appender makes that easy for you, and provides the 'infect_die' configuration property
522             to do so:
523              
524             ...
525             log4perl.appender.Raven.infect_die=1
526             ...
527              
528             This is heavily inspired by L<https://metacpan.org/pod/Log::Log4perl::FAQ#My-program-already-uses-warn-and-die-.-How-can-I-switch-to-Log4perl>
529              
530             While this can be convenient to quickly implement this in a non-log4perl aware piece of software, you
531             are strongly encourage not to use this feature and pepper your call with appropriate Log4perl calls.
532              
533             =head1 SEE ALSO
534              
535             L<Sentry::Raven> , L<Log::Log4perl>, L<Log::Any> , L<Log::Any::Adapter::Log4perl>
536              
537             =head1 AUTHOR
538              
539             Jerome Eteve jeteve@cpan.com
540              
541             =cut