Group
Extension

PagerDuty-Agent/lib/PagerDuty/Agent.pm

package PagerDuty::Agent;

use 5.010;
use strict;
use warnings;
use Data::Dump 'dump';
use Moo;
use MooX::Types::MooseLike::Base qw/ ArrayRef Int Str /;

our $VERSION = '0.02';

use English '-no_match_vars';
use HTTP::Request::Common 'POST';
use JSON;
use LWP::UserAgent;
use Sys::Hostname;
use Time::Piece;

=head1 NAME

PagerDuty::Agent - A perl PagerDuty client

=head1 VERSION

Version 0.02

=head1 SYNOPSIS

  use PagerDuty::Agent;

  my $agent = PagerDuty::Agent->new( routing_key => '3fcc9112463424b599f996f9e780dfc6' );

  # trigger an event, then resolve it
  my $dedup_key = $agent->trigger_event( 'something is terribly wrong!' );

  if ( $dedup_key ) {
    print "Event created, dedup_key = $dedup_key\n";

    print "Event successfully resolved\n"
      if $agent->resolve_event( $dedup_key );
  } else {
    warn "Failed to submit event: $@\n";
  }

  # additional context can be passed in
  $agent->trigger_event(
    summary   => 'something is terribly wrong!',
    severity  => 'critical',
    dedup_key => 'abc123',
  );

=head1 DESCRIPTION

This module implements the Events API for submitting events to PagerDuty.

=head1 CONSTRUCTOR

=head2 my $agent = PagerDuty::Agent->new( %options )

=over

=item C<< routing_key => '3fcc9112463424b599f996f9e780dfc6' >>

The routing key or integration key associated with the API integration, found when
viewing the service integration on the PagerDuty site.

=item C<< timeout => 5 >>

Do not wait longer than this number of seconds when attempting to send an event.

=item C<< api_version => 2 >>

Only version 2 is supported.

=back

=cut

has [qw/ post_url routing_key /] => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

has api_version => (
    is      => 'ro',
    isa     => Int,
    default => 2,
);

has timeout => (
    is      => 'ro',
    isa     => Int,
    default => 5,
);

has json_serializer => (
    is      => 'ro',
    builder => '_build_json_serializer',
    lazy    => 1,
);

has ua_obj => (
    is      => 'ro',
    builder => '_build_ua_obj',
    lazy    => 1,
);

has valid_severities => (
    is      => 'ro',
    isa     => ArrayRef[Str],
    default => sub { [qw/ critical error warning info /] },
);

around BUILDARGS => sub {
    my ($orig, $class, %args) = @_;

    my $routing_key = $args{routing_key}
        or die "must pass routing_key\n";

    delete($args{routing_key});

    my $timeout = delete($args{timeout});

    my $api_version = delete($args{api_version});
    $api_version = 2 unless defined($api_version);

    my $post_url = _post_url_for_version($api_version)
        or die "invalid api version $api_version\n";

    my $ua_obj = delete($args{ua_obj});

    return $class->$orig(
        routing_key => $routing_key,
        post_url => $post_url,

        (defined($api_version) ? (api_version => $api_version) : ()),
        (defined($timeout) ? (timeout => $timeout) : ()),
        (defined($ua_obj) ? (ua_obj => $ua_obj) : ()),
    );
};

=head1 EVENT API

These methods are designed to create and manipulate events.

=head2 my $dedup_key = $agent->trigger_event( $event_summary or %event )

Trigger an event.  The simple form accepts an $event_summary string with textual
details of the event.  The long form accepts additional event context.

When successful, returns the dedup_key.  On error, returns undef and sets $@.

Event parameters when using the long form:

=over

=item C<< summary => 'Server is on fire' >>

Required.  A textual description of the event.

=item C<< class => 'cpu load' >>

The type of event.

=item C<< component => 'mysql' >>

The mechanism responsible for the event.

=item C<< custom_details => { user => 'me' } >>

A hash-ref of key value pairs containing any additional details.

=item C<< dedup_key => 'my unique identifier' >>

This is used for threading like events as well as identifying events already triggered.
If this is not given, one will be generated by the upstream API.

=item C<< group => 'app-stack' >>

The grouping of components.

=item C<< images => [ { src => 'https://img.memecdn.com/silly-humans_o_842106.jpg' } ] >>

One or more images, each specified as a hash-ref containing:

=over

=item C<< src => 'image url' >>

Required.  Must be HTTPS.

=item C<< href => 'link url' >>

Make the image link click-able.

=item C<< alt => 'some alt text' >>

Add alt text to the image.

=back

=item C<< links => [ { text => 'see the docs', href => 'https://google.com' } ] >>

One or more links, each specified as a hash-ref containing:

=over

=item C<< href => 'https://google.com' >>

Required.  Link destination.

=item C<< text => 'click here' >>

Required.  Link text.

=back

=item C<< severity => 'error' >>

The severity of the event.  Can be one of critical, error, warning, or info.  Defaults to error.

=item C<< source => 'google.com' >>

The hostname from which this event was triggered.  Defaults to the current hostname.

=item C<< timestamp => '2017-07-12T12:50:22.000-0700' >>

The event timestamp.  This must be a valid ISO 8601 in the complete long form such as the
example.  This defaults to the current local time.


=back

=cut

sub trigger_event {
    my ($self, @params) = @_;

    @params = (summary => $params[0])
        if scalar(@params) == 1;

    return $self->_post_event(
        $self->_format_pd_cef('trigger', @params),
    );
}

=head2 my $success = $agent->acknowledge_event( $dedup_key or %event )

Acknowledge an existing event.  The simple form accepts a $dedup_key.  The long
form accepts the same event parameters as C<< trigger_event >> except C<< summary >>
is interpreted as the reason for acknowledging and C<< dedup_key >> is required.

When successful, returns the dedup_key.  On error, returns undef and sets $@.

=cut

sub acknowledge_event {
    my ($self, @params) = @_;

    @params = (summary => 'no reason given', dedup_key => $params[0])
        if scalar(@params) == 1;

    return $self->_post_event(
        $self->_format_pd_cef('acknowledge', @params),
    );
}

=head2 my $success = $agent->resolve_event( $dedup_key or %event )

This accepts the same parameters as C<< acknowledge_event >> and returns the
same values.

=cut

sub resolve_event {
    my ($self, @params) = @_;

    @params = (summary => 'no reason given', dedup_key => $params[0])
        if scalar(@params) == 1;

    return $self->_post_event(
        $self->_format_pd_cef('resolve', @params),
    );
}

sub _post_event {
    my ($self, $event) = @_;

    unless ($event) {
        $EVAL_ERROR = "unable to parse event parameters";
        warn "$EVAL_ERROR\n";
        return;
    }

    my ($response, $response_code, $response_content);

    eval {
        $self->ua_obj()->timeout($self->timeout());

        my $request = POST(
            $self->post_url(),
            'Content-Type'  => 'application/json',
            'Authorization' => 'Token token='.$self->routing_key(),
            Content         => $self->json_serializer()->encode($event),
        );
        $response = $self->ua_obj()->request($request);

        $response_code = $response->code();
        $response_content = $response->content();
    };

    warn "$EVAL_ERROR\n" if $EVAL_ERROR;

    if ($response && $response->is_success()) {
        return $self->json_serializer()->decode($response_content)->{dedup_key};
    } else {
        if ($response) {
            my $error_message;
            eval {
                $error_message =  dump(
                    $self->json_serializer()->decode($response_content)
                );
            };

            $EVAL_ERROR = "Unable to parse response from PagerDuty: $error_message"
                if $error_message;
        }

        return;
    }
}

sub _validate_severity {
    my ($self, $severity) = @_;

    return unless defined($severity);

    my %severity_hash = map { $_ => 1 } @{ $self->valid_severities() };

    if (exists($severity_hash{$severity})) {
        return $severity;
    } else {
        warn "unknown severity: $severity\n";
        return;
    }
};

sub _build_json_serializer { JSON->new()->utf8(1)->pretty(1)->allow_nonref(1) }

sub _build_ua_obj {
    return LWP::UserAgent->new(
        keep_alive => 1,
    );
}

sub _post_url_for_version {
    my ($version) = @_;
    return unless defined($version);
    return {
        2 => 'https://events.pagerduty.com/v2/enqueue',
    }->{$version};
}

sub _trim {
    my ($string, $length) = @_;
    return defined($string)
        ? substr($string, 0, $length)
        : undef;
}

sub _format_pd_cef {
    my ($self, $event_action, @params) = @_;

    my %params;

    if (scalar(@params) % 2 == 0) {
        %params = @params;
    } else {
        return;
    }

    $self->_validate_severity($params{severity})
        if defined($params{severity});

    return {
        routing_key  => $self->routing_key(),
        event_action => $event_action,
        dedup_key    => $params{dedup_key},

        images => $params{images},
        links  => $params{links},

        payload => {
            summary        => $params{summary},
            source         => $params{source}     || hostname(),
            severity       => $params{severity}   || 'error',
            timestamp      => $params{timestamp}  || localtime()->strftime('%FT%T.000%z'),
            component      => $params{component},
            group          => $params{group},
            class          => $params{class},
            custom_details => $params{custom_details},
        },
    };
}

=head1 See Also

L<https://v2.developer.pagerduty.com/docs/events-api-v2> - The PagerDuty Events V2 API documentation

L<WebService::PagerDuty> - Another module implementing most of the PagerDuty Events V1 API.

=head1 LICENSE

Copyright (C) 2019 by Matt Harrington

The full text of this license can be found in the LICENSE file included with this module.

=cut

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.