Introduction

Kannel, a robust open-source SMS and WAP gateway, to control SMS traffic. Performance monitoring is essential to guarantee dependable message delivery and spot bottlenecks. This blog post will show you how to create a PHP script (kannel_monitor.php) that retrieves Kannel status information from a public endpoint (such as https://example.com/?kannel), saves it in a MySQL database, and uses a cron job to run every five minutes. We’ll also handle common production issues, such as the dreaded “Invalid response: Kannel status not found” error, and set up logging to /var/log/kannel_monitor.log for debugging.

Why Monitor Kannel?

Kannel’s status page provides real-time metrics, such as:

  • SMSC Connections: Details about each SMS center (SMSC), including protocol, IP, ports, and status.
  • SMS Metrics: Received, sent, queued, and failed messages, with rates (messages per second).
  • DLR Metrics: Delivery report (DLR) statistics for tracking message delivery.

Our script will fetch this data, parse it, store it in a database, and log operations for troubleshooting.

Step 1: Designing the Script

The kannel_monitor.php script will:

  • Fetch Kannel status data using file_get_contents.
  • Parse metrics (SMSC connections, SMS, and DLR) using regular expressions.
  • Store the data in a MySQL database.
  • Log operations to /var/log/kannel_monitor.log.
  • Run every 5 minutes via a cron job.
<?php

// Fetch status text
$context = stream_context_create([
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true
    ]
]);
$statusText = @file_get_contents($kannelUrl, false, $context);
if ($statusText === false) {
    $error = error_get_last();
    error_log("Failed to fetch status from $kannelUrl: " . ($error['message'] ?? 'Unknown error'), 3, $logFile);
    exit(1);
}

// Debug: Save response to file
file_put_contents(__DIR__ . '/debug_kannel_status.html', $statusText);

// Validate response
if (strpos($statusText, 'Kannel bearerbox version') === false && strpos($statusText, 'SMSC:') === false) {
    error_log("Invalid response: Kannel status not found. Check URL or response format.", 3, $logFile);
    exit(1);
}

// Initialize metrics
$smsReceivedTotal = 0;
$smsSentTotal = 0;
$smsQueuedReceived = 0;
$smsQueuedSent = 0;
$smsInboundRateAvg = 0.0;
$smsOutboundRateAvg = 0.0;
$dlrReceivedTotal = 0;
$dlrSentTotal = 0;
$dlrQueued = 0;
$dlrInboundRateAvg = 0.0;
$smscRecords = [];

// Parse SMSC connections
preg_match_all(
    '/(\w+\[\w+\])\s+(SMPP|HTTP|FAKE):([^:]+):(\d+)\/(\d+):([^:]*):?(SMPP|XCV)?\s+\((\w+)[^,]+,\s*rcvd: sms (\d+).*?\(([\d.]+),([\d.]+),([\d.]+)\).*?sent: sms (\d+).*?\(([\d.]+),([\d.]+),([\d.]+)\).*?dlr (\d+).*?\(([\d.]+),([\d.]+),([\d.]+)\).*?failed (\d+), queued (\d+)/',
    $statusText, $smscMatches, PREG_SET_ORDER
);
foreach ($smscMatches as $match) {
    $smscRecords[] = [
        'provider_name' => $match[1],
        'protocol' => $match[2],
        'ip_address' => $match[3] == 'generic' ? null : $match[3],'
        'smsc_status' => $match[8],
        'provider_sms_received' => (int) $match[9],
        'provider_dlr_received_rate_avg' => (float) $match[19],
        'provider_sms_failed' => (int) $match[21],
        'provider_sms_queued' => (int) $match[22]
    ];
}

// Parse aggregate metrics
if (preg_match('/SMS: received (\d+) \((\d+) queued\), sent (\d+) \((\d+) queued\)/', $statusText, $matches)) {
    $smsReceivedTotal = (int) $matches[1];
    $smsQueuedReceived = (int) $matches[2];
    $smsSentTotal = (int) $matches[3];
    $smsQueuedSent = (int) $matches[4];
}
if (preg_match('/SMS: inbound \(([\d.]+),([\d.]+),([\d.]+)\) msg\/sec, outbound \(([\d.]+),([\d.]+),([\d.]+)\) msg\/sec/', $statusText, $matches)) {
    $smsInboundRateAvg = (float) $matches[2];
    $smsOutboundRateAvg = (float) $matches[5];
}
if (preg_match('/DLR: received (\d+), sent (\d+)/', $statusText, $matches)) {
    $dlrReceivedTotal = (int) $matches[1];
    $dlrSentTotal = (int) $matches[2];
}
if (preg_match('/DLR: (\d+) queued/', $statusText, $matches)) {
    $dlrQueued = (int) $matches[1];
}
if (preg_match('/DLR: inbound \(([\d.]+),([\d.]+),([\d.]+)\) msg\/sec/', $statusText, $matches)) {
    $dlrInboundRateAvg = (float) $matches[2];
}

// Connect to database
$db = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
if ($db->connect_error) {
    error_log("DB connection failed: " . $db->connect_error, 3, $logFile);
    exit(1);
}

// Insert records
$stmt = $db->prepare("
    INSERT INTO kannel_monitor (
        timestamp, provider_name, smsc_status, protocol, ip_address, port_transmit, port_receive, system_id,
        provider_sms_received, provider_sms_sent, provider_dlr_received, provider_sms_failed, provider_sms_queued,
        provider_sms_received_rate_avg, provider_sms_sent_rate_avg, provider_dlr_received_rate_avg,
        sms_received_total, sms_sent_total, sms_queued_received, sms_queued_sent,
        sms_inbound_rate_avg, sms_outbound_rate_avg, dlr_received_total, dlr_sent_total, dlr_queued,
        dlr_inbound_rate_avg
    )
    VALUES (NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
if (!$stmt) {
    error_log("Prepare failed: " . $db->error, 3, $logFile);
    $db->close();
    exit(1);
}

foreach ($smscRecords as $record) {
    $stmt->bind_param(
        'ssssiiisiiiidddiiiiddiiid',
        $record['provider_name'],
        $record['smsc_status'],
        $record['protocol'],
        $record['ip_address'],
        $record['port_transmit'],
        $smsQueuedSent,
        $smsInboundRateAvg,
        $smsOutboundRateAvg,
        $dlrReceivedTotal,
        $dlrSentTotal,
        $dlrQueued,
        $dlrInboundRateAvg
    );
    if (!$stmt->execute()) {
        error_log("Insert failed for {$record['provider_name']}: " . $db->error, 3, $logFile);
        break;
    } else {
        error_log("Inserted record for {$record['provider_name']}", 3, $logFile);
    }
}
// Insert aggregate metrics if no SMSC records
if (empty($smscRecords)) {
    $nullStr = null;
    $nullInt = null;
    $nullFloat = null;
    $stmt->bind_param(
        'ssssiiisiiiidddiiiiddiiid',
        $nullStr,
        $nullStr,
        $nullStr,
        $nullInt,
        $nullFloat,
        $nullFloat,
        $nullFloat,
        $smsReceivedTotal,
        $smsSentTotal,
        $smsQueuedReceived,
        $smsQueuedSent,
        $smsInboundRateAvg,
        $smsOutboundRateAvg,
        $dlrReceivedTotal,
        $dlrSentTotal,
        $dlrQueued,
        $dlrInboundRateAvg
    );
    if (!$stmt->execute()) {
        error_log("Insert failed for aggregate metrics: " . $db->error, 3, $logFile);
    } else {
        error_log("Inserted aggregate metrics", 3, $logFile);
    }
}

$stmt->close();
$db->close();

error_log("Data stored successfully", 3, $logFile)

Step 2: The Script

Complete kannel_monitor.php script, which I developed to monitor a Kannel instance at https://example.com/?kannel. It includes error handling, logging, and validation to avoid issues like the “Invalid response” error I encountered in production.

Step 3: Setting Up the Environment

To run this script in production, follow these steps:

  1. Save the Script: Save the script to /var/www/html/scripts/kannel_monitor.php:
    Paste the script above and set permissions: chmod 644 /var/www/html/scripts/kannel_monitor.php chown www-data:www-data /var/www/html/scripts/kannel_monitor.php
  2. Create .env File: KANNEL_URL=https://example.com/?
    kannel DB_HOST=localhost
    DB_USER=your_db_user
    DB_PASS=your_db_password
    DB_NAME=your_db_name
  3. Create the Log File: Set up /var/log/kannel_monitor.log: sudo touch /var/log/kannel_monitor.log sudo chmod 664 /var/log/kannel_monitor.log sudo chown www-data:www-data /var/log/kannel_monitor.log
  4. Set Up the Cron Job: Schedule the script to run every 5 minutes: crontab -e Add: */5 * * * * /usr/bin/php /var/www/html/scripts/kannel_monitor.php >> /var/log/kannel_monitor.log 2>&1 Verify: crontab -l

Step 4: Testing the Script

Test the script manually:

/usr/bin/php /var/www/html/scripts/kannel_monitor.php

Check the log file:
cat /var/log/kannel_monitor.log

Expected output:
Inserted record for provider1[smsc1]
Inserted aggregate metrics
Data stored successfully

It should contain Kannel status data (e.g., SMSC:, SMS: received).

Verify database records:
SELECT * FROM kannel_monitor;

Wait 5 minutes to confirm the cron job runs, or temporarily set it to run every minute:
* * * * /usr/bin/php /var/www/html/scripts/kannel_monitor.php >> /var/log/kannel_monitor.log 2>&1


Conclusion

Using kannel_monitor.php with a cron job is an easy way to keep track of your SMS gateway. The script grabs, processes, and saves data reliably, with logs to help fix problems.

Leave a Reply