File Coverage

blib/lib/PagerDuty/Agent.pm
Criterion Covered Total %
statement 76 88 86.3
branch 18 28 64.2
condition 8 11 72.7
subroutine 21 22 95.4
pod 3 3 100.0
total 126 152 82.8


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