File Coverage

blib/lib/Log/Log4perl/Appender/Raven.pm
Criterion Covered Total %
statement 113 120 94.1
branch 24 40 60.0
condition 13 22 59.0
subroutine 17 18 94.4
pod 0 2 0.0
total 167 202 82.6


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