Incident Reporting with PagerDuty and Vonage
Published on May 10, 2021

Introduction

PagerDuty is an incident reporting management system that provides notifications and automatic escalations to help engineering teams detect and fix issues as they arise with their infrastructure. In this post, we’ll be looking at how we can use Vonage to alert members of an engineering team whenever an incident occurs on any of their infrastructure.

Using the Vonage Dispatch API, which provides an automatic failover option, we’ll first send out notifications via Facebook Messenger as the primary medium and then SMS as a fallback option.

Technical Requirements

To follow along, you’ll need the following:

  • PHP version 7.1 or Higher

  • Laravel 5.8

  • Ngrok which allows you to expose your local web server to the internet. To learn more about how to set up your local environment with Ngrok, you can check out the docs here.

  • Composer

  • Vonage CLI

  • A Facebook Account

  • A PagerDuty Account

DT API Account

To complete this tutorial, you will need a DT API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the DT API Dashboard.

Setting up

To be able to use the Dispatch API, there are a few things we need to do with our Vonage account. Head over to your Vonage dashboard and under the Messages and Dispatch section, create a new messages application.

new message app in dashboardnew message app in dashboard

\

  • The Status URL is the webhook endpoint Vonage will make a POST request to whenever we send out SMS notifications. This will allow us to determine the status update of the SMS that has been sent out i.e delivered, rejected, or submitted. Since we’ll be using Ngrok, the status URL should be something like this qc43v7.ngrok.io/webhooks/status.

  • The Inbound URL is the endpoint Vonage will make a request to whenever inbound messages are received and should look similar to this qc43v7.ngrok.io/webhooks/inbound-message

  • Next, generate a public/private key pair. This will automatically download the private key file for you as well. Take note of this file as we’ll be needing it shortly.

  • Create the application and then take note of the application id. Next, you’ll be prompted to associate numbers and external accounts to this application. This is totally optional, so you can go just go with the default and then create the application.

Setting up PagerDuty

Head over to PagerDuty and create an account if you don’t already have one. We’ll also need to create a Service. A Service in PagerDuty represents anything we would like to open incidents against. It could be an application, a component, or even a team. So whenever a new incident occurs on that Service, we want to send out webhook notifications to a particular endpoint in our application.

In your PagerDuty dashboard, head over to Configuration -> Services and create a new Service.

pager duty dashboardpager duty dashboard

Since we’ll be making use of our own custom integration, select use our API directly for the integration type, and then select Events API V2 and fill out all the other necessary details. You can read more about creating a service on PagerDuty here.

Next, after you’ve successfully created the service, under the Integrations tab, select New Extension and then give it a type of Generic V2 webhook. The details section will consist of the endpoint in our application where we want to receive notifications from PagerDuty and should be something like qc43v7.ngrok.io/webhooks/incident

pager dutypager duty

Setting Up Laravel

We’ll be using composer to install a new Laravel project. From the command line, create a new Laravel project using the following command:

composer create-project --prefer-dist laravel/laravel vonage-pager-duty

Routes and Controller

Edit the routes/web.php with the following code:

<!--?php

Route::post('/webhooks/inbound-message', 'WebhookController@inboundMessage');
Route::post('/webhooks/status', 'WebhookController@status');
Route::post('/webhooks/incident', 'WebhookController@report');
</code-->

Cross-site request forgery attack is a type of malicious attack whereby unauthorized commands are carried out on behalf of an authenticated user. To prevent such attacks, Laravel automatically generates a `Csrf` token for every authenticated user which helps to validate that the authenticated user is the one actually making a request to the application. However, since these requests will be coming from outside the application and we trust the source, we need to disable `Csrf` validation for these routes. The `VerifyCsrfToken` middleware is used for validating all tokens. Luckily, the middleware accepts an `except` array which contains a list of all routes to disable CSRF verification for. Edit the `app\Http\Middleware\VerifyCsrfToken.php` file with the following code: ```blok {"type":"codeBlock","props":{"lang":"php","code":"protected%20$except%20=%20%5B%0A%20%20%20%20%20%20%20%20'/webhooks/*'%0A%20%20%20%20%5D;%0A"}} ``` Next, run the following command to create a controller: ```blok {"type":"termynal","props":{"code":"%3Cspan%20data-ty=%22input%22%3Ephp%20artisan%20make:controller%20WebhookController%20%3C/span%3E"}} ``` This will generate a `WebhookController.php` file for us in the `app\Http\Controllers` directory. Edit the file with the following code: ```blok {"type":"codeBlock","props":{"lang":"php","code":"%3C!--?php%0A%0Anamespace%20App%5CHttp%5CControllers;%0A%0Ause%20Illuminate%5CHttp%5CRequest;%0Ause%20Illuminate%5CSupport%5CFacades%5CLog;%0Ause%20App%5CJobs%5CReportIncident;%0A%0Aclass%20WebhookController%20extends%20Controller%0A%7B%0A%20%20%20%20public%20function%20inboundMessage(Request%20$request)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20Log::debug('Inbound%20Message',%20$request---%3Eall());%0A%20%20%20%20%7D%0A%0A%20%20%20%20public%20function%20status(Request%20$request)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20Log::debug('Status',%20$request-&gt;all());%0A%20%20%20%20%7D%0A%0A%20%20%20%20public%20function%20report(Request%20$request)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20Log::debug('Incident',%20$request-&gt;all());%0A%0A%20%20%20%20%20%20%20%20dispatch(new%20ReportIncident());%0A%0A%20%20%20%20%20%20%20%20return%20response('Webhook%20received');%0A%20%20%20%20%7D%0A%7D%0A"}} ``` For the `inboundMessage()` and `status()` methods, we simply log the request data from Vonage to the log file Laravel provides. The `report()` method dispatches a `ReportIncident` job which we shall be creating shortly. This job is responsible for triggering SMS notifications using a custom Wrapper around the Vonage API. ### Linking Facebook With Your Vonage Account To be able to successfully send messages from Vonage to Facebook, you’ll first need to link a Facebook page associated with your Facebook account to your Vonage account. You can learn how to do that [here](/messages/concepts/facebook#link-your-facebook-page-to-your-nexmo-account?utm_campaign=dev_spotlight&utm_content=incident_Pagerduty_Jolaoso). When you’re done with the linking, send a message from your Facebook account to the Facebook page. Since it’s an inbound message, Vonage will send a request containing details of the message to the `/webhooks/inbound-message` endpoint we created earlier. Check your logs file and you should see an entry similar to the following: ```blok {"type":"codeBlock","props":{"lang":"php","code":"Inbound%20Message%7B%20%20%0A%20%20%20%22message_uuid%22:%220a2088d2-e028-4aa0-aa4a-ae11a6f82fb0%22,%0A%20%20%20%22to%22:%7B%20%20%0A%20%20%20%20%20%20%22id%22:%221923256201474167%22,%0A%20%20%20%20%20%20%22type%22:%22messenger%22%0A%20%20%20%7D,%0A%20%20%20%22from%22:%7B%20%20%0A%20%20%20%20%20%20%22id%22:%2223037543461244470%22,%0A%20%20%20%20%20%20%22type%22:%22messenger%22%0A%20%20%20%7D,%0A%20%20%20%22timestamp%22:%222019-08-06T21:56:16.887Z%22,%0A%20%20%20%22direction%22:%22inbound%22,%0A%20%20%20%22message%22:%7B%20%20%0A%20%20%20%20%20%20%22content%22:%7B%20%20%0A%20%20%20%20%20%20%20%20%20%22type%22:%22text%22,%0A%20%20%20%20%20%20%20%20%20%22text%22:%22Hello%22%0A%20%20%20%20%20%20%7D%0A%20%20%20%7D%0A%7D%0A"}} ``` The Facebook sender id is the `to.id`, while the recipient id is the `from.id`. Take note of these details as we’ll be needing them shortly. ### Creating a Custom Wrapper Since the Dispatch API is still in beta as at the time of writing this tutorial, the Vonage PHP library doesn’t yet provide support for it. As a result, we’ll be using a custom wrapper to interact with Nexmo’s API. To get started, we first need to generate a Json Web Token (JWT) which we’ll use for authenticating with the API. Using the [Vonage CLI](https://github.com/Nexmo/nexmo-cli), run the following command: ```blok {"type":"termynal","props":{"code":"%3Cspan%20data-ty=%22input%22%3Evonage%20jwt%20--key_file=./private.key%20--app_id=VONAGE_APPLICATION_ID%3C/span%3E"}} ``` `./private.key` is the path to the private key file that was generated for you when you created the Messages and Dispatch application, while the `app_id` is the application id we took note of earlier. Next, copy the output of this command. ### Environment and Config Variables Add the JWT you just generated to your `.env`, the messenger details we noted earlier and the phone numbers that will handle sending and receiving SMS notifications. ```blok {"type":"codeBlock","props":{"lang":"javascript","code":"VONAGE_JWT%20=%20xxxx%0AFB_SENDER_ID%20=%20xxxx%0AFB_RECIPIENT_ID%20=%20xxxx%0ASMS_FROM%20=%20xxxx%0ASMS_TO%20=%20xxxx%0A"}} ``` Next, we will reference the environment variables we just defined through our config file. Head over to the `config` directory and under the `services.php` file, add a new Vonage `array`. ```blok {"type":"codeBlock","props":{"lang":"javascript","code":"'vonage'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20'jwt'%20=&gt;%20env('VONAGE_JWT'),%0A%20%20%20%20%20%20%20%20'fb_sender_id'%20=&gt;%20env('FB_SENDER_ID'),%0A%20%20%20%20%20%20%20%20'fb_recipient_id'%20=&gt;%20env('FB_RECIPIENT_ID'),%0A%20%20%20%20%20%20%20%20'sms_from'%20=&gt;%20env('SMS_FROM'),%0A%20%20%20%20%20%20%20%20'sms_to'%20=&gt;%20env('SMS_TO')%0A%20%20%20%20%5D%0A"}} ``` ### Installing Dependency The only dependency our project will have is the [GuzzleHTTP](http://docs.guzzlephp.org/) library which we’ll use for making API calls. Run the following command to install the library: ```blok {"type":"termynal","props":{"code":"%3Cspan%20data-ty=%22input%22%3Ecomposer%20require%20guzzlehttp/guzzle%3C/span%3E"}} ``` ### Creating the Wrapper In the `app` directory, create a `Vonage.php` file and add the following code to the file: ```blok {"type":"codeBlock","props":{"lang":"php","code":"%3C!--?php%0A%0Anamespace%20App;%0A%0Ause%20GuzzleHttp%5CClient;%0A%0Aclass%20Vonage%0A%7B%0A%20%20%20%20protected%20$client;%0A%0A%20%20%20%20public%20function%20__construct()%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20$this---%3Eclient%20=%20$this-&gt;setUpClient();%0A%20%20%20%20%7D%0A%0A%20%20%20%20protected%20function%20setUpClient()%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20$authBearer%20=%20'Bearer%20'%20.%20config('services.vonage.jwt');%0A%0A%20%20%20%20%20%20%20%20return%20new%20Client(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20'base_uri'%20=&gt;%20'https://api.nexmo.com',%0A%20%20%20%20%20%20%20%20%20%20%20%20'headers'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'Authorization'%20=&gt;%20$authBearer,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'Content-Type'%20=&gt;%20'application/json',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'Accept'%20=&gt;%20'application/json'%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%5D);%0A%20%20%20%20%7D%0A%0A%0A%20%20%20%20public%20function%20dispatch()%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20$response%20%20=%20$this-&gt;client-&gt;request('POST',%20'/v0.1/dispatch',%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20'json'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'template'%20=&gt;%20'failover',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'workflow'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'from'%20=&gt;%20%5B%20'type'%20=&gt;%20'messenger','id'%20=&gt;%20config('services.vonage.fb_recipient_id')%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'to'%20=&gt;%20%5B'type'%20=&gt;%20'messenger',%20'id'%20=&gt;%20config('services.vonage.fb_sender_id')%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'message'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'content'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'type'%20=&gt;%20'text',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'text'%20=&gt;%20'An%20incident%20just%20occurred',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'failover'%20=&gt;%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'expiry_time'%20=&gt;%2015,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'condition_status'%20=&gt;%20'read',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'from'%20=&gt;%20%5B'type'%20=&gt;%20'sms','number'%20=&gt;%20config('services.vonage.sms_from')%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'to'%20=&gt;%20%5B'type'%20=&gt;%20'sms','number'%20=&gt;%20config('services.vonage.sms_to')%5D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'message'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'content'%20=&gt;%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'type'%20=&gt;%20'text',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20'text'%20=&gt;%20'An%20incident%20just%20occurred',%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%5D);%0A%0A%20%20%20%20%20%20%20%20return%20json_decode($response-&gt;getBody());%0A%20%20%20%20%7D%0A%7D%0A"}} ``` In the `dispatch()` method, we’ve defined a failover template and a workflow. Whenever an incident occurs via PagerDuty, we’ll get a notification about the incident on Facebook Messenger. If the notification isn’t read within 15 seconds, the failover condition is triggered and an SMS notification is sent as a fallback. ### Creating the Job Run the following command on the terminal to create a new job class: ```blok {"type":"termynal","props":{"code":"%3Cspan%20data-ty=%22input%22%3Ephp%20artisan%20make:job%20ReportIncident%3C/span%3E"}} ``` Edit the `app\Http\Jobs\ReportIncident` file with the following code: ```blok {"type":"codeBlock","props":{"lang":"php","code":"%3C!--?php%0A%0Anamespace%20App%5CJobs;%0A%0Ause%20Illuminate%5CBus%5CQueueable;%0Ause%20Illuminate%5CQueue%5CSerializesModels;%0Ause%20Illuminate%5CQueue%5CInteractsWithQueue;%0Ause%20Illuminate%5CContracts%5CQueue%5CShouldQueue;%0Ause%20Illuminate%5CFoundation%5CBus%5CDispatchable;%0Ause%20App%5CVonage;%0A%0Aclass%20ReportIncident%20implements%20ShouldQueue%0A%7B%0A%20%20%20%20use%20Dispatchable,%20InteractsWithQueue,%20Queueable,%20SerializesModels;%0A%0A%20%20%20%20/**%0A%20%20%20%20%20*%20Create%20a%20new%20job%20instance.%0A%20%20%20%20%20*%0A%20%20%20%20%20*%20@return%20void%0A%20%20%20%20%20*/%0A%20%20%20%20public%20function%20__construct()%0A%20%20%20%20%7B%0A%20%20%20%20%7D%0A%0A%20%20%20%20/**%0A%20%20%20%20%20*%20Execute%20the%20job.%0A%20%20%20%20%20*%0A%20%20%20%20%20*%20@return%20void%0A%20%20%20%20%20*/%0A%20%20%20%20public%20function%20handle(Voange%20$vonage)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20$vonage---%3Edispatch();%0A%20%20%20%20%7D%0A%7D%0A"}} ``` The `handle()` method receives the custom Vonage wrapper we created earlier on as a dependency and then we call the dispatch method on the class. Bringing It All Together ------------------------ It’s time to finally test what we’ve been building so far. Head over to your PagerDuty dashboard and create a new incident. ![incidents screenshot](https://d226lax1qjow5r.cloudfront.net/blog/blogposts/incident-reporting-with-pagerduty-and-nexmo/incidents.png "incidents screenshot") This will trigger a webhook notification to be sent out to the endpoint we added earlier to our PagerDuty account which will, in turn, send out a message to the Facebook page we linked earlier to our Vonage account. Assuming everything went fine, you should inspect your log file as Vonage would have sent an update as regards the status of the message we just submitted. ```blok {"type":"codeBlock","props":{"lang":"javascript","code":"%7B%20%20%0A%20%20%20%22message_uuid%22:%22c1bcf89b-c16e-427f-a4b7-15816327832f%22,%0A%20%20%20%22to%22:%7B%20%20%0A%20%20%20%20%20%20%22id%22:%221923256201474167%22,%0A%20%20%20%20%20%20%22type%22:%22messenger%22%0A%20%20%20%7D,%0A%20%20%20%22from%22:%7B%20%20%0A%20%20%20%20%20%20%22id%22:%2223037543461244470%22,%0A%20%20%20%20%20%20%22type%22:%22messenger%22%0A%20%20%20%7D,%0A%20%20%20%22timestamp%22:%222019-08-08T01:53:07.922Z%22,%0A%20%20%20%22status%22:%22read%22,%0A%20%20%20%22_links%22:%7B%20%20%0A%20%20%20%20%20%20%22dispatch%22:%7B%20%20%0A%20%20%20%20%20%20%20%20%20%22href%22:%22v0.1/dispatch/15756412-30d6-4664-8a1e-abcd029ea7a4%22,%0A%20%20%20%20%20%20%20%20%20%22dispatch_uuid%22:%2215756412-30d6-4664-8a1e-abcd029ea7a4%22%0A%20%20%20%20%20%20%7D%0A%20%20%20%7D%0A%7D,%0A%7B%20%20%0A%20%20%20%22template%22:%22failover%22,%0A%20%20%20%22status%22:%22completed%22,%0A%20%20%20%22timestamp%22:%222019-08-08T01:53:07.959Z%22,%0A%20%20%20%22usage%22:%7B%20%20%0A%20%20%20%20%20%20%22price%22:%220.003%22,%0A%20%20%20%20%20%20%22currency%22:%22EUR%22%0A%20%20%20%7D,%0A%20%20%20%22dispatch_uuid%22:%2215756412-30d6-4664-8a1e-abcd029ea7a4%22,%0A%20%20%20%22_links%22:%7B%20%20%0A%20%20%20%20%20%20%22messages%22:%5B%20%20%0A%20%20%20%20%20%20%20%20%20%7B%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%22message_uuid%22:%22c1bcf89b-c16e-427f-a4b7-15816327832f%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22href%22:%22v0.1/messages/c1bcf89b-c16e-427f-a4b7-15816327832f%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22channel%22:%22messenger%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22usage%22:%7B%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22price%22:%220.001%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22currency%22:%22EUR%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22status%22:%22read%22%0A%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%5D%0A%20%20%20%7D%0A%7D%0A"}} ``` From the above logs, we first get a status update informing us that the message has been read and then subsequently another status update informing us that the dispatch workflow has been completed. Since the message was read within 15 seconds, the failover condition was satisfied; as a result, the SMS notification was never triggered. To test that the SMS notification will be sent out if the failover condition isn’t met, You can repeat the same process but this time do not read the Facebook messenger notification. You’ll discover that this time the SMS notification will be triggered. Conclusion ---------- In this tutorial, we’ve seen how we can build a custom integration with PagerDuty and Vonage to ensure a resilient fall back alert system in the case of an emergency. You can find a link to the Github repo [here](https://github.com/Dotunj/nexmo-pager-duty).

Dotun Jolaoso

Software Developer who loves building awesome tools and products. I currently work with Laravel, PHP and Vue.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.