Skip to main content

Blockchain Alerts on PagerDuty

Updated on
Sep 30, 2024

Overview

PagerDuty is a robust incident management platform that helps detect and respond to critical issues in real time. By automating alert notifications, escalations, and on-call scheduling, PagerDuty ensures that the right people are alerted at the right time, minimizing downtime and improving overall operational efficiency.

In this Function example, we will set up a blockchain data stream to send data and alert to PagerDuty, using QuickNode Streams and Functions.

Sample Function

We will establish the data stream using QuickNode Streams. Streams offer various popular data solutions such as Webhook, Postgres, AWS S3, Snowflake, etc. However, there are instances where you may require a specific or custom destination. With Functions, we can create blockchain serverless functions that send data to custom destinations. Furthermore, Streams ensures guaranteed data delivery with retries on delivery failures. We have implemented error handling using try-catch blocks in our Function's code. This means that if something goes wrong with PagerDuty, our Stream will attempt redelivery via Function, and any errors will also be logged on the Streams dashboard.

Now, let's create a Function that receives gas price data from Streams and sends it to PagerDuty.

The first step woulf be to create a .env file to store your PagerDuty routing key.

PAGERDUTY_ROUTING_KEY=YOUR_PAGERDUTY_KEY

Replace YOUR_PAGERDUTY_KEY with your PagerDuty routing key.

Now, create an index.js and paste the following code in it:

require('dotenv').config();
const https = require('https');
const url = require('url');

const PAGERDUTY_EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue';
const ROUTING_KEY = process.env.PAGERDUTY_ROUTING_KEY;

if (!ROUTING_KEY) {
console.error('PAGERDUTY_ROUTING_KEY environment variable is not set');
process.exit(1);
}

function sendToPagerDuty(payload) {
return new Promise((resolve, reject) => {
const parsedUrl = new url.URL(PAGERDUTY_EVENTS_API_URL);

const options = {
method: 'POST',
hostname: parsedUrl.hostname,
path: parsedUrl.pathname,
headers: {
'Content-Type': 'application/json'
}
};

const req = https.request(options, (res) => {
let data = '';

res.on('data', (chunk) => {
data += chunk;
});

res.on('end', () => {
console.log(`PagerDuty API Response: ${res.statusCode} ${res.statusMessage}`);
console.log(`Response body: ${data}`);

if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(data));
} else {
reject(new Error(`HTTP Error: ${res.statusCode} ${res.statusMessage}\nBody: ${data}`));
}
});
});

req.on('error', (error) => {
console.error('Error in HTTPS request:', error);
reject(error);
});

console.log('Sending payload to PagerDuty:', JSON.stringify(payload, null, 2));
req.write(JSON.stringify(payload));
req.end();
});
}

async function main(params) {
console.log('Starting main function with params:', JSON.stringify(params, null, 2));

// Test trigger for PagerDuty
if (params.test) {
console.log('Test trigger activated');
const testPayload = {
routing_key: ROUTING_KEY,
event_action: 'trigger',
payload: {
summary: 'Test Alert from QuickNode Function',
source: 'QuickNode Ethereum Stream',
severity: 'info'
}
};
try {
const response = await sendToPagerDuty(testPayload);
console.log('Test incident created:', response);
return {
message: "Test PagerDuty alert sent successfully",
incident: response
};
} catch (error) {
console.error('Error sending test PagerDuty alert:', error);
throw error;
}
}

let averageGasPriceInGwei, blockNumber;

// Check if params is an object and has the expected structure
if (typeof params === 'object' && params !== null) {
// If the data is nested inside a 'data' property
if (params.data && typeof params.data === 'object') {
({ averageGasPriceInGwei, blockNumber } = params.data);
} else {
// If the data is directly in the params object
({ averageGasPriceInGwei, blockNumber } = params);
}
}

// Validate the extracted data
if (typeof averageGasPriceInGwei !== 'number' ||
typeof blockNumber !== 'number') {
console.error('Invalid or missing data in params:', { averageGasPriceInGwei, blockNumber });
throw new Error('Invalid or missing data in params');
}

console.log(`Current gas price: ${averageGasPriceInGwei.toFixed(2)} Gwei at block ${blockNumber}`);

const summary = `Gas Price Update: ${averageGasPriceInGwei.toFixed(2)} Gwei at block ${blockNumber}`;

const incidentPayload = {
routing_key: ROUTING_KEY,
event_action: 'trigger',
payload: {
summary: summary,
source: 'QuickNode Ethereum Stream',
severity: 'info',
custom_details: {
averageGasPriceInGwei: averageGasPriceInGwei.toFixed(2),
blockNumber: blockNumber
}
}
};

try {
const response = await sendToPagerDuty(incidentPayload);
console.log('Incident created:', response);
return {
message: "PagerDuty alert sent successfully",
incident: response
};
} catch (error) {
console.error('Error sending PagerDuty alert:', error);
throw error;
}
}

module.exports = { main };

The above script receives gas price data from QuickNode Stream whenever it is higher than a preset threshold (10 Gwei in this case), extracts it, and send an alert to PagerDuty along with gas price data and block number.

Zip the Files

Since we're using a .env file, we will need to upload a .zip file of index.js, .env, package.json, and package-lock.json along with node_modules folder so that our function can work on the QuickNode Functions platform.

Learn more about QuickNode Functions Code Editor and .Zip file uploading.

Set up a QuickNode Stream

For our example, we'll stream gas price data above a preset threshold. But, with Streams, you can do all kinds of things, from getting raw data to highly granular and enriched data; the possibilities are endless. You can check some examples in our backfill library.

The following is the Streams Filter being used:

Try it yourself by deploying the filtered Stream to your account.

function main(data) {
const block = data.streamData;
const transactions = block.transactions;
const blockNumber = parseInt(block.number, 16); // Convert block number from hex to int

// Variable to define the maximum number of transactions to consider
const numberOfTransactions = 100; // You can change this value as needed

// Calculate the average gas price for the first 'numberOfTransactions' transactions
let totalGasPrice = 0;
const totalTransactions = Math.min(transactions.length, numberOfTransactions);
for (let i = 0; i < totalTransactions; i++) {
const gasPriceInWei = parseInt(transactions[i].gasPrice, 16);
totalGasPrice += gasPriceInWei;
}

const averageGasPriceInWei = totalGasPrice / totalTransactions;
const averageGasPriceInGwei = averageGasPriceInWei / 1e9; // Convert Wei to Gwei

// Only return a result if the average gas price is above 10 Gwei
if (averageGasPriceInGwei > 10) {
return {
averageGasPriceInGwei,
blockNumber
};
}

// Return null if the condition is not met
return null;
}

Now, while selecting a destination for your QuickNode Stream, select the previously created Function as the destination for your Stream and create the Stream. Once the Stream starts streaming the gas price data above the set threshold to Function, the Function will send alerts and data to PagerDuty. It'll look something like this:

Alerts on PagerDuty

Data on PagerDuty

Learn more about QuickNode Functions.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this doc