File Coverage

blib/lib/Log/Log4perl/Appender/Raven.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Log::Log4perl::Appender::Raven;
2             $Log::Log4perl::Appender::Raven::VERSION = '0.004';
3 5     5   23763 use Moose;
  0            
  0            
4              
5             use Carp;
6             use Data::Dumper;
7             use Sentry::Raven;
8             use Log::Log4perl;
9             use Devel::StackTrace;
10              
11             has 'sentry_dsn' => ( is => 'ro', isa => 'Maybe[Str]' );
12             has 'sentry_timeout' => ( is => 'ro' , isa => 'Int' ,required => 1 , default => 1 );
13             has 'infect_die' => ( is => 'ro' , isa => 'Bool', default => 0 );
14              
15             has 'raven' => ( is => 'ro', isa => 'Sentry::Raven', lazy_build => 1);
16              
17             # STATIC CONTEXT
18             has 'context' => ( is => 'ro' , isa => 'HashRef', default => sub{ {}; });
19              
20             # STATIC TAGS. They will go in the global context.
21             has 'tags' => ( is => 'ro' ,isa => 'HashRef', default => sub{ {}; });
22              
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             my %L4P2SENTRY = ('ALL' => 'info',
33             'TRACE' => 'debug',
34             'DEBUG' => 'debug',
35             'INFO' => 'info',
36             'WARN' => 'warning',
37             'ERROR' => 'error',
38             'FATAL' => 'fatal');
39              
40             sub BUILD{
41             my ($self) = @_;
42             if( $self->infect_die() ){
43             warn q|INFECTING SIG __DIE__ with Log4perl trickery. Ideally you should not count on that.
44              
45             See perldoc Log::Log4perl::Appender::Raven, section 'CODE WIHTOUT LOG4PERL'
46              
47             |;
48              
49             # Infect die. This is based on http://log4perl.sourceforge.net/releases/Log-Log4perl/docs/html/Log/Log4perl/FAQ.html#73200
50             $SIG{__DIE__} = sub{
51              
52             ## Are we called from within log4perl at all.
53             {
54             my $frame_up = 0;
55             while( my @caller = caller($frame_up++) ){
56             if( $caller[0] =~ /^Log::Log4perl/ ){
57             return;
58             }
59             }
60             }
61              
62              
63             ## warn "CALLING die Handler";
64             my $method = 'fatal';
65              
66             my $level_up = 1;
67              
68             # In an eval, nothing is fatal:
69             if( $^S ){
70             $method = 'error';
71             }
72              
73             my ($package, $filename, $line,
74             $subroutine, @discard ) = caller(0);
75             # warn "CALLER PACKAGE IS $package\n";
76             # warn "CALLER SUBROUTINE IS $subroutine";
77             if( $package =~ /^Carp/ ){
78             # One level up please. We dont want to make Carp the culprit.
79             # and we want to know which is the calling package (to get the logger).
80             ($package, @discard ) = caller(1);
81             $level_up++ ;
82             }
83              
84             my $logger = Log::Log4perl->get_logger($package || '');
85              
86             ## This will make sure the following error or
87             ## fatal level work as usual.
88             local $Log::Log4perl::caller_depth =
89             $Log::Log4perl::caller_depth + $level_up ;
90              
91             $logger->$method(@_);
92              
93             if( $method eq 'error' ){
94             # Do not die. This will be catched by the enclosing eval.
95             return undef;
96             }
97              
98             # Not in an eval, die for good.
99             die @_;
100             };
101             }
102             }
103              
104              
105             sub _build_raven{
106             my ($self) = @_;
107              
108             my $dsn = $self->sentry_dsn || $ENV{SENTRY_DSN} || confess("No sentry_dsn config or SENTRY_DSN in ENV");
109              
110              
111             my %raven_context = %{$self->context()};
112             $raven_context{tags} = $self->tags();
113              
114             return Sentry::Raven->new( sentry_dsn => $dsn,
115             timeout => $self->sentry_timeout,
116             %raven_context
117             );
118             }
119              
120             sub log{
121             my ($self, %params) = @_;
122              
123             ## Any logging within this method will be discarded.
124             if( Log::Log4perl::MDC->get(__PACKAGE__.'-reentrance') ){
125             return;
126             }
127             Log::Log4perl::MDC->put(__PACKAGE__.'-reentrance', 1);
128              
129             # use Data::Dumper;
130             # warn Dumper(\%params);
131              
132             # Look there to see what sentry expects:
133             # http://sentry.readthedocs.org/en/latest/developer/client/index.html#building-the-json-packet
134              
135             my $sentry_message = length($params{message}) > 1000 ? substr($params{message}, 0 , 1000) : $params{message};
136             my $sentry_logger = $params{log4p_category};
137             my $sentry_level = $L4P2SENTRY{$params{log4p_level}} || 'info';
138              
139             # We are 4 levels down after the standard Log4perl caller_depth
140             my $caller_offset = Log::Log4perl::caller_depth_offset( $Log::Log4perl::caller_depth + 4 );
141              
142             ## Stringify arguments NOW. This avoid sending huuge objects when
143             ## Serializing this stack trace inside Sentry::Raven
144             my $caller_frames = Devel::StackTrace->new( no_refs => 1);
145             {
146             ## Remove the frames from the Log4Perl layer.
147             my @frames = $caller_frames->frames();
148             splice(@frames, 0, $caller_offset);
149             $caller_frames->frames(@frames);
150             }
151              
152             my $sentry_culprit = 'main';
153             {
154             my $call_depth = $caller_offset;
155             # Go up the caller ladder until the first non eval
156             while( my @caller_info = caller($call_depth++) ){
157              
158             # Skip evals and __ANON__ methods.
159             # The anon method will make that compatible with the new Log::Any (>0.15)
160             my $caller_string = $caller_info[3] || '';
161              
162             unless( ( $caller_string eq '(eval)' )
163             || ( scalar(reverse($caller_string)) =~ /^__NONA__/ )
164             # ^ This test for the caller string to end with __ANON__ , but faster.
165             ){
166             # This is good.
167             # Subroutine name, or filename, or just main
168             $sentry_culprit = $caller_info[3] || $caller_info[1] || 'main';
169             last;
170             }
171             }
172             }
173              
174             my $tags = {};
175             if( my $mdc_tags = $self->mdc_tags() ){
176             $tags = Log::Log4perl::MDC->get($mdc_tags) || {};
177             }
178              
179             my $extra = {};
180             if( my $mdc_extra = $self->mdc_extra() ){
181             $extra = Log::Log4perl::MDC->get($mdc_extra) || {};
182             }
183              
184             my $user;
185             if( my $mdc_user = $self->mdc_user() ){
186             $user = Log::Log4perl::MDC->get($mdc_user);
187             }
188              
189             my $http;
190             if( my $mdc_http = $self->mdc_http() ){
191             $http = Log::Log4perl::MDC->get($mdc_http);
192             }
193              
194             # OK WE HAVE THE BASIC Sentry options.
195             $self->raven->capture_message($sentry_message,
196             logger => $sentry_logger,
197             level => $sentry_level,
198             culprit => $sentry_culprit,
199             tags => $tags,
200             extra => $extra,
201             Sentry::Raven->stacktrace_context( $caller_frames ),
202             ( $user ? Sentry::Raven->user_context(%$user) : () ),
203             ( $http ? Sentry::Raven->request_context( ( delete $http->{url} ) , %$http ) : () )
204             );
205              
206             Log::Log4perl::MDC->put(__PACKAGE__.'-reentrance', undef);
207             }
208              
209              
210             __PACKAGE__->meta->make_immutable();
211              
212              
213             =head1 NAME
214              
215             Log::Log4perl::Appender::Raven - Append log events to your Sentry account.
216              
217             =head1 BUILD STATUS
218              
219             =begin html
220              
221             <a href="https://travis-ci.org/jeteve/l4p-appender-raven"><img src="https://travis-ci.org/jeteve/l4p-appender-raven.svg?branch=master"></a>
222              
223             =end html
224              
225             =head1 WARNING(s)
226              
227             This appender will send ALL the log events it receives to your
228             Sentry DSN synchronously. If you generate a lot of logging, that can make your sentry account
229             saturate quite quickly and your application come to a severe slowdown.
230              
231             Using Log4perl appender's Threshold or L<Log::Log4perl::Filter> in your log4perl config, and
232             experimenting a little bit is Highly Recommended.
233              
234             Remember sentry is designed to record errors, so hopefully your application will
235             not generate too many of them.
236              
237             You have been warned.
238              
239             =head1 SYNOPSIS
240              
241             Read the L<CONFIGURATION> section, then use Log4perl just as usual.
242              
243             If you are not familiar with Log::Log4perl, please check L<Log::Log4perl>
244              
245             In a nutshell, here's the minimul l4p config to output anything from ERROR to Sentry:
246              
247             log4perl.rootLogger=DEBUG, Raven
248              
249             log4perl.appender.Raven=Log::Log4perl::Appender::Raven
250             log4perl.appender.Raven.Threshold=ERROR
251             log4perl.appender.Raven.sentry_dsn="https://user:key@sentry-host.com/project_id"
252             log4perl.appender.Raven.layout=Log::Log4perl::Layout::PatternLayout
253             log4perl.appender.Raven.layout.ConversionPattern=%X{chunk} %d %F{1} %L> %m %n
254              
255              
256             =head1 CONFIGURATION
257              
258             This is just another L<Log::Log4perl::Appender>.
259              
260             =head2 Simple Configuration
261              
262             The only mandatory configuration key
263             is *sentry_dsn* which is your sentry dsn string obtained from your sentry account.
264             See http://www.getsentry.com/ and https://github.com/getsentry/sentry for more details.
265              
266             Alternatively to setting this configuration key, you can set an environment variable SENTRY_DSN
267             with the same setting. - Not recommended -
268              
269             Example:
270              
271             log4perl.rootLogger=ERROR, Raven
272              
273             layout_class=Log::Log4perl::Layout::PatternLayout
274             layout_pattern=%X{chunk} %d %F{1} %L> %m %n
275              
276             log4perl.appender.Raven=Log::Log4perl::Appender::Raven
277             log4perl.appender.Raven.sentry_dsn="http://user:key@host.com/project_id"
278             log4perl.appender.Raven.sentry_timeout=1
279             log4perl.appender.Raven.layout=${layout_class}
280             log4perl.appender.Raven.layout.ConversionPattern=${layout_pattern}
281              
282             =head2 Timeout
283              
284             The default timeout is 1 second. Feel free to bump it up. If sending an event
285             timesout (or if the sentry host is down or doesn't exist), a plain Perl
286             warning will be output.
287              
288             =head2 Configuration with Static Tags
289              
290             You have the option of predefining a set of tags that will be send to
291             your Sentry installation with every event. Remember Sentry tags have a name
292             and a value (they are not just 'labels').
293              
294             Example:
295              
296             ...
297             log4perl.appender.Raven.tags.application=myproduct
298             log4perl.appender.Raven.tags.installation=live
299             ...
300              
301             =head2 Configure and use Dynamic Tagging
302              
303             Dynamic tagging is performed using the Log4Perl MDC mechanism.
304             See L<Log::Log4perl::MDC> if you are not familiar with it.
305              
306             Anywhere in your code.
307              
308             ...
309             Log::Log4perl::MDC->set('sentry_tags' , { subsystem => 'my_subsystem', ... });
310             $log->error("Something very wrong");
311             ...
312              
313             Or specify which key to capture in config:
314              
315             ...
316             log4perl.appender.Raven.mdc_tags=my_sentry_tags
317             ...
318              
319              
320             Note that tags added this way will be added to the statically define ones, or override them in case
321             of conflict.
322              
323             Note: Tags are meant to categorize your Sentry events and will be displayed
324             in the Sentry GUI like any other category.
325              
326             =head2 Configure and use User Data
327              
328             Sentry supports structured user data that can be added to your event.
329             User data works a bit like the tags, except only three keys are supported:
330              
331             id, username and email. See L<Sentry::Raven> (capture_user) for a description of those keys.
332              
333              
334             In your code:
335              
336             ...
337             Log::Log4perl::MDC->set('sentry_user' , { id => '123' , email => 'jeteve@cpan.org', username => 'jeteve' });
338             $log->error("Something very wrong");
339             ...
340              
341              
342             Or specify the MDC key to capture in Config:
343              
344             ...
345             log4perl.appender.Raven.mdc_user=my_sentry_user
346             ...
347              
348              
349             =head2 Configure and use HTTP Request data.
350              
351             Sentry support HTTP Request structured data that can be added to your event.
352             HTTP Data work a bit like tags, except only a number of keys are supported:
353              
354             url, method, data, query_string, cookies, headers, env
355              
356             See L<Sentry::Raven> (capture_request) or interface 'Http' in L<http://sentry.readthedocs.org/en/latest/developer/interfaces/index.html>
357             for a full description of those keys.
358              
359             In your code:
360              
361             ...
362             Log::Log4perl::MDC->set('sentry_http' , { url => 'http://www.example.com' , method => 'GET' , ... });
363             $log->error("Something very wrong");
364             ...
365              
366             Or specify the MDC key to capture in Config:
367              
368             ...
369             log4perl.appender.Raven.mdc_http=my_sentry_http
370             ...
371              
372             =head2 Configure and use Dynamic Extra
373              
374             Sentry allows you to specify any data (as a Single level HashRef) that will be stored with the Event.
375              
376             It's very similar to dynamic tags, except its not tags.
377              
378             Then anywere in your code:
379              
380             ...
381             Log::Log4perl::MDC->set('my_sentry_extra' , { session_id => ... , ... });
382             $log->error("Something very wrong");
383             ...
384              
385              
386             Or specify MDC key to capture in config:
387              
388             ...
389             log4perl.appender.Raven.mdc_extra=my_sentry_extra
390             ...
391              
392             =head2 Configuration with a Static Context.
393              
394             You can use lines like:
395              
396             log4perl.appender.Raven.context.platform=myproduct
397              
398             To define static L<Sentry::Raven> context. The list of context keys supported is not very
399             long, and most of them are defined dynamically when you use this package anyway.
400              
401             See L<Sentry::Raven> for more details.
402              
403             =head1 USING Log::Any
404              
405             This is tested to work with Log::Any just the same way it works when you use Log4perl directly.
406              
407             =head1 CODE WITHOUT LOG4PERL
408              
409             Warning: Experimental feature.
410              
411             If your code, or some of its dependencies is not using Log4perl, you might want
412             to consider infecting the __DIE__ pseudo signal with some amount of trickery to have die (and Carp::confess/croak)
413             calls go through log4perl.
414              
415             This appender makes that easy for you, and provides the 'infect_die' configuration property
416             to do so:
417              
418             ...
419             log4perl.appender.Raven.infect_die=1
420             ...
421              
422             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>
423              
424             While this can be convenient to quickly implement this in a non-log4perl aware piece of software, you
425             are strongly encourage not to use this feature and pepper your call with appropriate Log4perl calls.
426              
427             =head1 SEE ALSO
428              
429             L<Sentry::Raven> , L<Log::Log4perl>, L<Log::Any> , L<Log::Any::Adapter::Log4perl>
430              
431             =head1 AUTHOR
432              
433             Jerome Eteve jeteve@cpan.com
434              
435             =cut