File Coverage

blib/lib/PagerDuty/Agent.pm
Criterion Covered Total %
statement 73 86 84.8
branch 18 28 64.2
condition 8 11 72.7
subroutine 20 21 95.2
pod 3 3 100.0
total 122 149 81.8


line stmt bran cond sub pod time code
1             package PagerDuty::Agent;
2              
3 5     5   124293 use 5.008;
  5         16  
4 5     5   23 use strict;
  5         8  
  5         87  
5 5     5   18 use warnings;
  5         9  
  5         110  
6 5     5   1310 use Moo;
  5         34815  
  5         27  
7 5     5   6053 use MooX::Types::MooseLike::Base qw/ ArrayRef Int Str /;
  5         24632  
  5         477  
8              
9             our $VERSION = '0.01';
10              
11 5     5   758 use English '-no_match_vars';
  5         4140  
  5         35  
12 5     5   3350 use HTTP::Request::Common 'POST';
  5         77326  
  5         318  
13 5     5   1981 use JSON;
  5         34269  
  5         32  
14 5     5   2910 use LWP::UserAgent;
  5         86682  
  5         187  
15 5     5   1538 use Sys::Hostname;
  5         4209  
  5         226  
16 5     5   1574 use Time::Piece;
  5         27863  
  5         24  
17              
18             =head1 NAME
19              
20             PagerDuty::Agent - A perl PagerDuty client
21              
22             =head1 VERSION
23              
24             Version 0.01
25              
26             =head1 SYNOPSIS
27              
28             use PagerDuty::Agent;
29              
30             my $agent = PagerDuty::Agent->new( routing_key => '3fcc9112463424b599f996f9e780dfc6' );
31              
32             # trigger an event, then resolve it
33             my $dedup_key = $agent->trigger_event( 'something is terribly wrong!' );
34              
35             if ( $dedup_key ) {
36             print "Event created, dedup_key = $dedup_key\n";
37              
38             print "Event successfully resolved\n"
39             if $agent->resolve_event( $dedup_key );
40             }
41              
42             # additional context can be passed in
43             $agent->trigger_event(
44             summary => 'something is terribly wrong!',
45             severity => 'critical',
46             dedup_key => 'abc123',
47             );
48              
49             =head1 DESCRIPTION
50              
51             This module implements the Events API for submitting events to PagerDuty.
52              
53             =head1 CONSTRUCTOR
54              
55             =head2 my $agent = PagerDuty::Agent->new( %options )
56              
57             =over
58              
59             =item C<< routing_key => '3fcc9112463424b599f996f9e780dfc6' >>
60              
61             The routing key or integration key associated with the API integration, found when
62             viewing the service integration on the PagerDuty site.
63              
64             =item C<< timeout => 5 >>
65              
66             Do not wait longer than this number of seconds when attempting to send an event.
67              
68             =item C<< api_version => 2 >>
69              
70             Only version 2 is supported.
71              
72             =back
73              
74             =cut
75              
76             has [qw/ post_url routing_key /] => (
77             is => 'ro',
78             isa => Str,
79             required => 1,
80             );
81              
82             has api_version => (
83             is => 'ro',
84             isa => Int,
85             default => 2,
86             );
87              
88             has timeout => (
89             is => 'ro',
90             isa => Int,
91             default => 5,
92             );
93              
94             has json_serializer => (
95             is => 'ro',
96             builder => '_build_json_serializer',
97             lazy => 1,
98             );
99              
100             has ua_obj => (
101             is => 'ro',
102             builder => '_build_ua_obj',
103             lazy => 1,
104             );
105              
106             has valid_severities => (
107             is => 'ro',
108             isa => ArrayRef[Str],
109             default => sub { [qw/ critical error warning info /] },
110             );
111              
112             around BUILDARGS => sub {
113             my ($orig, $class, %args) = @_;
114              
115             my $routing_key = $args{routing_key}
116             or die "must pass routing_key\n";
117              
118             delete($args{routing_key});
119              
120             my $timeout = delete($args{timeout});
121              
122             my $api_version = delete($args{api_version});
123             $api_version = 2 unless defined($api_version);
124              
125             my $post_url = _post_url_for_version($api_version)
126             or die "invalid api version $api_version\n";
127              
128             my $ua_obj = delete($args{ua_obj});
129              
130             return $class->$orig(
131             routing_key => $routing_key,
132             post_url => $post_url,
133              
134             (defined($api_version) ? (api_version => $api_version) : ()),
135             (defined($timeout) ? (timeout => $timeout) : ()),
136             (defined($ua_obj) ? (ua_obj => $ua_obj) : ()),
137             );
138             };
139              
140             =head1 EVENT API
141              
142             These methods are designed to create and manipulate events.
143              
144             =head2 my $dedup_key = $agent->trigger_event( $event_summary or %event )
145              
146             Trigger an event. The simple form accepts an $event_summary string with textual
147             details of the event. The long form accepts additional event context.
148              
149             When successful, returns the dedup_key. On error, returns undef and sets $@.
150              
151             Event parameters when using the long form:
152              
153             =over
154              
155             =item C<< summary => 'Server is on fire' >>
156              
157             Required. A textual description of the event.
158              
159             =item C<< class => 'cpu load' >>
160              
161             The type of event.
162              
163             =item C<< component => 'mysql' >>
164              
165             The mechanism responsible for the event.
166              
167             =item C<< custom_details => { user => 'me' } >>
168              
169             A hash-ref of key value pairs containing any additional details.
170              
171             =item C<< dedup_key => 'my unique identifier' >>
172              
173             This is used for threading like events as well as identifying events already triggered.
174             If this is not given, one will be generated by the upstream API.
175              
176             =item C<< group => 'app-stack' >>
177              
178             The grouping of components.
179              
180             =item C<< images => [ { src => 'https://img.memecdn.com/silly-humans_o_842106.jpg' } ] >>
181              
182             One or more images, each specified as a hash-ref containing:
183              
184             =over
185              
186             =item C<< src => 'image url' >>
187              
188             Required. Must be HTTPS.
189              
190             =item C<< href => 'link url' >>
191              
192             Make the image link click-able.
193              
194             =item C<< alt => 'some alt text' >>
195              
196             Add alt text to the image.
197              
198             =back
199              
200             =item C<< links => [ { text => 'see the docs', href => 'https://google.com' } ] >>
201              
202             One or more links, each specified as a hash-ref containing:
203              
204             =over
205              
206             =item C<< href => 'https://google.com' >>
207              
208             Required. Link destination.
209              
210             =item C<< text => 'click here' >>
211              
212             Required. Link text.
213              
214             =back
215              
216             =item C<< severity => 'error' >>
217              
218             The severity of the event. Can be one of critical, error, warning, or info. Defaults to error.
219              
220             =item C<< source => 'google.com' >>
221              
222             The hostname from which this event was triggered. Defaults to the current hostname.
223              
224             =item C<< timestamp => '2017-07-12T12:50:22.000-0700' >>
225              
226             The event timestamp. This must be a valid ISO 8601 in the complete long form such as the
227             example. This defaults to the current local time.
228              
229              
230             =back
231              
232             =cut
233              
234             sub trigger_event {
235 5     5 1 1924 my ($self, @params) = @_;
236              
237 5 100       25 @params = (summary => $params[0])
238             if scalar(@params) == 1;
239              
240 5         21 return $self->_post_event(
241             $self->_format_pd_cef('trigger', @params),
242             );
243             }
244              
245             =head2 my $success = $agent->acknowledge_event( $dedup_key or %event )
246              
247             Acknowledge an existing event. The simple form accepts a $dedup_key. The long
248             form accepts the same event parameters as C<< trigger_event >> except C<< summary >>
249             is interpreted as the reason for acknowledging and C<< dedup_key >> is required.
250              
251             When successful, returns the dedup_key. On error, returns undef and sets $@.
252              
253             =cut
254              
255             sub acknowledge_event {
256 2     2 1 1609 my ($self, @params) = @_;
257              
258 2 100       7 @params = (summary => 'no reason given', dedup_key => $params[0])
259             if scalar(@params) == 1;
260              
261 2         5 return $self->_post_event(
262             $self->_format_pd_cef('acknowledge', @params),
263             );
264             }
265              
266             =head2 my $success = $agent->resolve_event( $dedup_key or %event )
267              
268             This accepts the same parameters as C<< acknowledge_event >> and returns the
269             same values.
270              
271             =cut
272              
273             sub resolve_event {
274 2     2 1 2693 my ($self, @params) = @_;
275              
276 2 100       11 @params = (summary => 'no reason given', dedup_key => $params[0])
277             if scalar(@params) == 1;
278              
279 2         9 return $self->_post_event(
280             $self->_format_pd_cef('resolve', @params),
281             );
282             }
283              
284             sub _post_event {
285 9     9   1043 my ($self, $event) = @_;
286              
287 9 50       26 unless ($event) {
288 0         0 $EVAL_ERROR = "unable to parse event parameters";
289 0         0 warn "$EVAL_ERROR\n";
290 0         0 return;
291             }
292              
293 9         15 my ($response, $response_code, $response_content);
294              
295 9         17 eval {
296 9         210 $self->ua_obj()->timeout($self->timeout());
297              
298 9         3583 my $request = POST(
299             $self->post_url(),
300             'Content-Type' => 'application/json',
301             'Authorization' => 'Token token='.$self->routing_key(),
302             Content => $self->json_serializer()->encode($event),
303             );
304 8         8797 $response = $self->ua_obj()->request($request);
305              
306 8         7370 $response_code = $response->code();
307 8         86 $response_content = $response->content();
308             };
309              
310 9 100       125 warn "$EVAL_ERROR\n" if $EVAL_ERROR;
311              
312 9 100 66     40 if ($response && $response->is_success()) {
313 8         276 return $self->json_serializer()->decode($response_content)->{dedup_key};
314             } else {
315 1 50       3 if ($response) {
316 0         0 my $error_message;
317 0         0 eval {
318 0         0 $error_message = $self->json_serializer()->decode($response_content)->{error}->{message};
319             };
320              
321 0 0       0 $error_message = "Unable to parse response from PagerDuty: $EVAL_ERROR"
322             if $EVAL_ERROR;
323              
324 0         0 $EVAL_ERROR = $error_message;
325             }
326              
327 1         12 return;
328             }
329             }
330              
331             sub _validate_severity {
332 1     1   5 my ($self, $severity) = @_;
333              
334 1 50       14 return unless defined($severity);
335              
336 1         4 my %severity_hash = map { $_ => 1 } @{ $self->valid_severities() };
  4         15  
  1         6  
337              
338 1 50       5 if (exists($severity_hash{$severity})) {
339 1         5 return $severity;
340             } else {
341 0         0 warn "unknown severity: $severity\n";
342 0         0 return;
343             }
344             };
345              
346 5     5   160 sub _build_json_serializer { JSON->new()->utf8(1)->pretty(1)->allow_nonref(1) }
347              
348             sub _build_ua_obj {
349 2     2   110 return LWP::UserAgent->new(
350             keep_alive => 1,
351             );
352             }
353              
354             sub _post_url_for_version {
355 10     10   20 my ($version) = @_;
356 10 50       24 return unless defined($version);
357             return {
358             2 => 'https://events.pagerduty.com/v2/enqueue',
359 10         57 }->{$version};
360             }
361              
362             sub _trim {
363 0     0   0 my ($string, $length) = @_;
364 0 0       0 return defined($string)
365             ? substr($string, 0, $length)
366             : undef;
367             }
368              
369             sub _format_pd_cef {
370 11     11   1484 my ($self, $event_action, @params) = @_;
371              
372 11         21 my %params;
373              
374 11 50       42 if (scalar(@params) % 2 == 0) {
375 11         45 %params = @params;
376             } else {
377 0         0 return;
378             }
379              
380             $self->_validate_severity($params{severity})
381 11 100       49 if defined($params{severity});
382              
383             return {
384             routing_key => $self->routing_key(),
385             event_action => $event_action,
386             dedup_key => $params{dedup_key},
387              
388             images => $params{images},
389             links => $params{links},
390              
391             payload => {
392             summary => $params{summary},
393             source => $params{source} || hostname(),
394             severity => $params{severity} || 'error',
395             timestamp => $params{timestamp} || localtime()->strftime('%FT%T.000%z'),
396             component => $params{component},
397             group => $params{group},
398             class => $params{class},
399             custom_details => $params{custom_details},
400             },
401 11   66     107 };
      100        
      66        
402             }
403              
404             =head1 See Also
405              
406             L<https://v2.developer.pagerduty.com/docs/events-api-v2> - The PagerDuty Events V2 API documentation
407              
408             L<WebService::PagerDuty> - Another module implementing most of the PagerDuty Events V1 API.
409              
410             =head1 LICENSE
411              
412             Copyright (C) 2017 by comScore, Inc
413              
414             The full text of this license can be found in the LICENSE file included with this module.
415              
416             =cut
417              
418             1;