Ingesting Logs from PHP

Send log entries to BunnyLogs from any PHP application using a custom log handler — no dependencies required beyond a standard PHP install.

How it works

BunnyLogs accepts log entries as an HTTP POST to your logspace endpoint. PHP's built-in stream_context_create + file_get_contents is all you need, or you can use Monolog — the de-facto standard logging library for PHP — to drop in a handler with almost no code.

The POST accepts three fields: message (required), level (DEBUG, INFO, WARNING, or ERROR — defaults to INFO), and program (a label for the source, defaults to php). Use the program field to distinguish subsystems — e.g. auth, payments, cron — so you can filter by service in the BunnyLogs dashboard.

Option 1 — Standalone (no dependencies)

Copy this helper into your project. It uses only PHP core functions and works on any host that allows outbound HTTPS.

<?php

function bunnylog(string $message, string $level = 'INFO', string $program = 'php'): void
{
    $uuid    = '<uuid>';   // replace with your logspace UUID
    $url     = "https://bunnylogs.com/live/{$uuid}";
    $payload = http_build_query([
        'message' => $message,
        'level'   => $level,
        'program' => $program,
    ]);

    $ctx = stream_context_create([
        'http' => [
            'method'        => 'POST',
            'header'        => "Content-Type: application/x-www-form-urlencoded\r\n",
            'content'       => $payload,
            'timeout'       => 3,
            'ignore_errors' => true,
        ],
    ]);

    @file_get_contents($url, false, $ctx);   // fire-and-forget; suppress errors
}

// Usage:
bunnylog('User signed up', 'INFO', 'auth');
bunnylog('Stripe webhook failed', 'ERROR', 'payments');
Option 2 — Monolog handler

If your project already uses Monolog (e.g. Laravel, Symfony, or a standalone install), add this handler class and wire it up in a few lines.

Install Monolog (skip if already present):

composer require monolog/monolog

BunnyLogsHandler.php

<?php

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;

class BunnyLogsHandler extends AbstractProcessingHandler
{
    private string $url;

    public function __construct(string $uuid, int|string|Level $level = Level::Debug, bool $bubble = true)
    {
        parent::__construct($level, $bubble);
        $this->url = "https://bunnylogs.com/live/{$uuid}";
    }

    protected function write(LogRecord $record): void
    {
        $levelMap = [
            'DEBUG'     => 'DEBUG',
            'INFO'      => 'INFO',
            'NOTICE'    => 'INFO',
            'WARNING'   => 'WARNING',
            'ERROR'     => 'ERROR',
            'CRITICAL'  => 'ERROR',
            'ALERT'     => 'ERROR',
            'EMERGENCY' => 'ERROR',
        ];

        $payload = http_build_query([
            'message' => $record->message,
            'level'   => $levelMap[$record->level->getName()] ?? 'INFO',
            'program' => $record->channel,
        ]);

        $ctx = stream_context_create([
            'http' => [
                'method'        => 'POST',
                'header'        => "Content-Type: application/x-www-form-urlencoded\r\n",
                'content'       => $payload,
                'timeout'       => 3,
                'ignore_errors' => true,
            ],
        ]);

        @file_get_contents($this->url, false, $ctx);
    }
}

Wire it up:

<?php

require 'vendor/autoload.php';
require 'BunnyLogsHandler.php';

use Monolog\Logger;

$log = new Logger('myapp');
$log->pushHandler(new BunnyLogsHandler('<uuid>'));

$log->info('Application started');
$log->warning('Disk space low', ['free_gb' => 1.2]);
$log->error('Database connection failed');
Laravel

In Laravel, place BunnyLogsHandler.php in app/Logging/ and add a custom channel to config/logging.php:

'bunnylogs' => [
    'driver' => 'monolog',
    'handler' => App\Logging\BunnyLogsHandler::class,
    'with' => [
        'uuid' => env('BUNNYLOGS_UUID'),
    ],
    'level' => env('LOG_LEVEL', 'warning'),
],

Then set LOG_CHANNEL=bunnylogs (or add it to a stack channel) in .env.

To include BunnyLogs alongside your existing channel rather than replacing it, use a stack channel:

'channels' => [
    'stack' => [
        'driver'   => 'stack',
        'channels' => ['daily', 'bunnylogs'],
    ],
    ...
],
Symfony

Register the handler as a service in config/services.yaml:

App\Logging\BunnyLogsHandler:
    arguments:
        $uuid: '%env(BUNNYLOGS_UUID)%'

Then reference it in config/packages/monolog.yaml:

monolog:
    handlers:
        bunnylogs:
            type: service
            id: App\Logging\BunnyLogsHandler
            level: warning
WordPress

Add the standalone helper to your theme's functions.php or a must-use plugin, then log anywhere in WordPress:

add_action('wp_login_failed', function (string $username) {
    bunnylog("Failed login for {$username}", 'WARNING', 'wordpress');
});

add_action('upgrader_process_complete', function () {
    bunnylog('Plugin/theme update completed', 'INFO', 'wordpress');
});
Registering a global error handler

To forward uncaught PHP errors and exceptions to BunnyLogs automatically, register handlers early in your bootstrap file:

<?php

set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
    $level = match (true) {
        in_array($errno, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR]) => 'ERROR',
        in_array($errno, [E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING])    => 'WARNING',
        default                                                               => 'INFO',
    };
    bunnylog("{$errstr} in {$errfile}:{$errline}", $level, 'php-error');
    return false; // let PHP's default handler run too
});

set_exception_handler(function (Throwable $e): void {
    bunnylog(
        get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(),
        'ERROR',
        'php-exception'
    );
});
High-traffic sites — non-blocking logging

The standalone helper uses a 3-second timeout and suppresses errors with @, which makes it effectively fire-and-forget for the vast majority of requests. However, on very high-traffic sites even a 3-second stall on a slow network call can accumulate. Two options to eliminate the block entirely:

  • Queue the HTTP call. In Laravel, dispatch a short queued job that calls the BunnyLogs endpoint. The HTTP call happens in a queue worker process and has zero impact on request latency.
  • Use a zero-timeout trick. Set timeout to 0.001 in the stream context. PHP opens the TCP connection, starts the request, then returns immediately when the timeout expires — the server still receives the full POST because the kernel handles the rest.
// Zero-timeout fire-and-forget (advanced)
$ctx = stream_context_create([
    'http' => [
        'method'        => 'POST',
        'header'        => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content'       => $payload,
        'timeout'       => 0.001,
        'ignore_errors' => true,
    ],
]);
@file_get_contents($url, false, $ctx);
Testing in development vs production

A clean pattern is to use a different logspace UUID per environment. Keep the UUID in an environment variable so you can switch without code changes:

// In .env.local
BUNNYLOGS_UUID=your-dev-uuid

// In .env.production
BUNNYLOGS_UUID=your-prod-uuid

This means your local development logs go to a separate stream, so they don't pollute your production view. You can share the development stream URL with a colleague for pair debugging without granting any access to production logs.

To disable BunnyLogs in automated test runs, simply set BUNNYLOGS_UUID to an empty string and guard the call:

function bunnylog(string $message, string $level = 'INFO', string $program = 'php'): void
{
    $uuid = getenv('BUNNYLOGS_UUID');
    if (!$uuid) return;
    // ... rest of the function
}
Tips
  • Store the UUID in an environment variable (BUNNYLOGS_UUID) rather than hard-coding it.
  • The timeout of 3 seconds prevents a slow BunnyLogs network call from blocking your users. For high-traffic sites, consider dispatching the HTTP call in a background job or queue.
  • Use the program field to distinguish between subsystems — e.g. auth, payments, cron — so you can filter by service in the BunnyLogs dashboard.
  • Monolog's channel name is used automatically as the program field by the handler above, so each new Logger('name') shows up as a distinct source.
  • For CLI scripts and cron jobs, the standalone helper works identically — PHP CLI allows outbound HTTPS by default on any hosting platform.
  • The BunnyLogs endpoint is CSRF-exempt and accepts application/x-www-form-urlencoded, JSON, and multipart — use whichever your HTTP client sends most naturally.