[PHP]syslog完全解説|システムログにメッセージを送信する方法と実践的な活用パターン

PHP

はじめに

アプリケーションのログをどこに記録するかは、運用において重要な設計判断です。独自のログファイルに書き込む方法もありますが、OSが提供するシステムログ機構にメッセージを送る選択肢もあります。

syslog() は、PHPからUnix系OSの syslog デーモン(rsyslogsyslog-ngなど)やWindowsのイベントログにメッセージを送信する関数です。openlog()closelog() と組み合わせて使うことで、優先度(重要度)やファシリティ(カテゴリ)を指定した構造化ログが実現できます。

本記事では関数の仕様から、実践的なログ設計パターンまで詳しく解説します。


関数の基本情報

項目内容
関数名syslog()
利用可能バージョンPHP 4以降
所属システム関数(Misc Functions)
戻り値bool(成功時true、失敗時false
拡張機能不要(コア関数。Windowsではイベントログとして動作)

シグネチャ

syslog(int $priority, string $message): bool

パラメータ

パラメータ説明
$priorityintログの優先度(重要度)。LOG_* 定数を使用
$messagestringログメッセージ本文

優先度(Priority)の一覧

syslogの優先度はUnixの標準仕様(RFC 5424)に基づいています。

定数意味使用場面の例
LOG_EMERG0システムが使用不能システム全体がダウン寸前
LOG_ALERT1即時対応が必要データ破損の検出など
LOG_CRIT2致命的な状態DB接続が完全に失われた等
LOG_ERR3エラー処理失敗・例外発生
LOG_WARNING4警告リトライ可能な異常・非推奨API使用
LOG_NOTICE5通知(正常だが注目すべき)設定変更・特権操作
LOG_INFO6情報通常の処理ログ
LOG_DEBUG7デバッグ情報詳細なトレース情報

数値が小さいほど重要度が高い点に注意してください。


ファシリティ(Facility)の一覧

openlog() で指定する「どのカテゴリのログか」を示す値です。

定数意味
LOG_USER一般的なユーザーレベルのメッセージ(デフォルト)
LOG_LOCAL0LOG_LOCAL7カスタム用途(アプリケーション独自のカテゴリ分け)
LOG_MAILメールシステム関連
LOG_DAEMONシステムデーモン関連
LOG_AUTH認証関連(セキュリティ)
LOG_CRONcron関連

Webアプリケーションでは LOG_USER または LOG_LOCAL0LOG_LOCAL7 を使うのが一般的です。


openlog() / closelog() との関係

openlog($ident, $option, $facility)  → syslog接続を開く(識別子・オプション・ファシリティを設定)
syslog($priority, $message)          → ログメッセージを送信(複数回呼べる)
closelog()                           → syslog接続を閉じる

openlog() のオプション定数

定数意味
LOG_PIDメッセージにプロセスIDを含める
LOG_CONSsyslogへの送信に失敗した場合、コンソールに出力
LOG_ODELAY最初のsyslog呼び出しまで接続をオープンしない(デフォルト)
LOG_NDELAYただちに接続をオープンする
LOG_PERRORログをsyslogだけでなく標準エラー出力にも出力

実践サンプル集(PHP 8.x対応)

サンプル1:基本的なログ送信

<?php
declare(strict_types=1);

// syslog接続を開く
openlog('myapp', LOG_PID | LOG_PERROR, LOG_USER);

// 各優先度でログを送信
syslog(LOG_INFO,    'アプリケーションが起動しました');
syslog(LOG_WARNING, 'API応答が通常より遅延しています(2.3秒)');
syslog(LOG_ERR,     'データベース接続に失敗しました');
syslog(LOG_DEBUG,   'デバッグ: リクエストパラメータ = ' . json_encode(['id' => 42]));

// 接続を閉じる
closelog();

echo "ログ送信完了(/var/log/syslog または journalctl で確認できます)\n";

確認方法(Linux環境):

$ sudo journalctl -t myapp --since "1 minute ago"
# または
$ tail -f /var/log/syslog | grep myapp

出力例:

Jun 24 10:30:00 myserver myapp[12345]: アプリケーションが起動しました
Jun 24 10:30:00 myserver myapp[12345]: API応答が通常より遅延しています(2.3秒)
Jun 24 10:30:00 myserver myapp[12345]: データベース接続に失敗しました
Jun 24 10:30:00 myserver myapp[12345]: デバッグ: リクエストパラメータ = {"id":42}

解説: LOG_PID を指定するとプロセスIDがログに自動付加され、LOG_PERROR で標準エラー出力にも同時出力されます。openlog()$ident (ここでは 'myapp')がログのタグとして使われ、フィルタリングに使えます。


サンプル2:クラスベースのロガー実装

PSR-3風のインターフェースで syslog() をラップした実用的なロガークラスです。

<?php
declare(strict_types=1);

class SyslogLogger
{
    private bool $isOpen = false;

    public function __construct(
        private readonly string $ident,
        private readonly int $facility = LOG_USER,
        private readonly bool $includeContext = true,
    ) {}

    private function ensureOpen(): void
    {
        if (!$this->isOpen) {
            openlog($this->ident, LOG_PID, $this->facility);
            $this->isOpen = true;
        }
    }

    public function emergency(string $message, array $context = []): bool
    {
        return $this->log(LOG_EMERG, $message, $context);
    }

    public function alert(string $message, array $context = []): bool
    {
        return $this->log(LOG_ALERT, $message, $context);
    }

    public function critical(string $message, array $context = []): bool
    {
        return $this->log(LOG_CRIT, $message, $context);
    }

    public function error(string $message, array $context = []): bool
    {
        return $this->log(LOG_ERR, $message, $context);
    }

    public function warning(string $message, array $context = []): bool
    {
        return $this->log(LOG_WARNING, $message, $context);
    }

    public function notice(string $message, array $context = []): bool
    {
        return $this->log(LOG_NOTICE, $message, $context);
    }

    public function info(string $message, array $context = []): bool
    {
        return $this->log(LOG_INFO, $message, $context);
    }

    public function debug(string $message, array $context = []): bool
    {
        return $this->log(LOG_DEBUG, $message, $context);
    }

    private function log(int $priority, string $message, array $context): bool
    {
        $this->ensureOpen();

        $formatted = $message;
        if ($this->includeContext && !empty($context)) {
            $formatted .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
        }

        return syslog($priority, $formatted);
    }

    public function __destruct()
    {
        if ($this->isOpen) {
            closelog();
        }
    }
}

// --- 使用例 ---
$logger = new SyslogLogger('myapp-web', LOG_LOCAL0);

$logger->info('リクエスト処理開始', ['method' => 'POST', 'path' => '/api/orders']);
$logger->warning('レート制限に接近', ['user_id' => 1024, 'requests' => 95, 'limit' => 100]);

try {
    throw new \RuntimeException('決済APIへの接続がタイムアウトしました');
} catch (\RuntimeException $e) {
    $logger->error($e->getMessage(), [
        'exception' => get_class($e),
        'trace_id'  => bin2hex(random_bytes(8)),
    ]);
}

$logger->debug('処理完了', ['duration_ms' => 245]);

echo "ログ記録完了\n";

確認方法:

$ journalctl -t myapp-web --since "1 minute ago"

出力例:

Jun 24 10:35:00 myserver myapp-web[12345]: リクエスト処理開始 {"method":"POST","path":"/api/orders"}
Jun 24 10:35:00 myserver myapp-web[12345]: レート制限に接近 {"user_id":1024,"requests":95,"limit":100}
Jun 24 10:35:01 myserver myapp-web[12345]: 決済APIへの接続がタイムアウトしました {"exception":"RuntimeException","trace_id":"a1b2c3d4e5f6a7b8"}
Jun 24 10:35:01 myserver myapp-web[12345]: 処理完了 {"duration_ms":245}

解説: PSR-3のログレベル名に合わせたメソッドを用意することで、Monologなど標準的なロガーと似た書き方ができます。JSON形式でコンテキストを付加することで、構造化ログとして後から jq 等で解析しやすくなります。


サンプル3:例外ハンドラとの統合

未処理の例外・エラーを自動的にsyslogへ記録するグローバルハンドラです。

<?php
declare(strict_types=1);

class SyslogExceptionHandler
{
    public function __construct(
        private readonly string $ident = 'php-app'
    ) {
        openlog($this->ident, LOG_PID, LOG_USER);
    }

    public function register(): void
    {
        set_exception_handler([$this, 'handleException']);
        set_error_handler([$this, 'handleError']);
        register_shutdown_function([$this, 'handleFatalError']);
    }

    public function handleException(\Throwable $e): void
    {
        $message = sprintf(
            '未処理の例外: %s: %s in %s:%d',
            get_class($e),
            $e->getMessage(),
            $e->getFile(),
            $e->getLine()
        );

        syslog(LOG_CRIT, $message);
        syslog(LOG_DEBUG, 'スタックトレース: ' . $e->getTraceAsString());

        echo "エラーが発生しました。管理者にログが送信されました。\n";
    }

    public function handleError(int $level, string $message, string $file = '', int $line = 0): bool
    {
        $priority = match (true) {
            $level & (E_ERROR | E_USER_ERROR)     => LOG_ERR,
            $level & (E_WARNING | E_USER_WARNING) => LOG_WARNING,
            $level & (E_NOTICE | E_USER_NOTICE)   => LOG_NOTICE,
            default                                => LOG_INFO,
        };

        syslog($priority, "PHPエラー: {$message} in {$file}:{$line}");

        // falseを返すと標準のエラーハンドラにも処理を委譲(記録のみが目的ならtrueでもよい)
        return false;
    }

    public function handleFatalError(): void
    {
        $error = error_get_last();
        if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
            syslog(
                LOG_EMERG,
                sprintf('致命的エラー: %s in %s:%d', $error['message'], $error['file'], $error['line'])
            );
        }
    }

    public function __destruct()
    {
        closelog();
    }
}

// --- 登録 ---
$handler = new SyslogExceptionHandler('myapp-errors');
$handler->register();

// --- デモ:意図的にエラーを発生させる ---
function riskyOperation(): void
{
    throw new \RuntimeException('外部APIから不正なレスポンスを受信しました');
}

try {
    riskyOperation();
} catch (\RuntimeException $e) {
    // 通常のtry-catchで捕捉した場合は明示的にログする
    syslog(LOG_ERR, "捕捉済み例外: " . $e->getMessage());
    echo "例外を適切に処理しました\n";
}

// 未捕捉の警告(自動でハンドラが呼ばれる)
@$undefined['key']; // E_WARNING(PHP 8ではTypeErrorだが、配列アクセスの例として記載)

解説: set_exception_handler()set_error_handler()register_shutdown_function() を組み合わせることで、アプリケーションのあらゆるエラーを自動的にsyslogへ記録できます。LOG_EMERG は致命的エラー(パースエラーなど)専用に予約し、優先度を適切に使い分けることが重要です。


サンプル4:マイクロサービス間のトレースIDを含む構造化ログ

分散システムでのリクエスト追跡に役立つパターンです。

<?php
declare(strict_types=1);

class TracedLogger
{
    private string $traceId;
    private string $serviceName;

    public function __construct(string $serviceName, ?string $traceId = null)
    {
        $this->serviceName = $serviceName;
        $this->traceId     = $traceId ?? $this->generateTraceId();

        openlog($serviceName, LOG_PID, LOG_LOCAL0);
    }

    private function generateTraceId(): string
    {
        return bin2hex(random_bytes(16));
    }

    public function getTraceId(): string
    {
        return $this->traceId;
    }

    public function info(string $event, array $data = []): void
    {
        $this->emit(LOG_INFO, $event, $data);
    }

    public function error(string $event, array $data = []): void
    {
        $this->emit(LOG_ERR, $event, $data);
    }

    public function span(string $operationName, callable $callback): mixed
    {
        $start = microtime(true);
        $this->info("{$operationName}.start");

        try {
            $result = $callback();
            $duration = round((microtime(true) - $start) * 1000, 2);
            $this->info("{$operationName}.success", ['duration_ms' => $duration]);
            return $result;
        } catch (\Throwable $e) {
            $duration = round((microtime(true) - $start) * 1000, 2);
            $this->error("{$operationName}.failure", [
                'duration_ms' => $duration,
                'error'       => $e->getMessage(),
            ]);
            throw $e;
        }
    }

    private function emit(int $priority, string $event, array $data): void
    {
        $payload = array_merge([
            'trace_id' => $this->traceId,
            'service'  => $this->serviceName,
            'event'    => $event,
            'time'     => date('c'),
        ], $data);

        syslog($priority, json_encode($payload, JSON_UNESCAPED_UNICODE));
    }

    public function __destruct()
    {
        closelog();
    }
}

// --- 使用例:複数サービスをまたぐ処理のシミュレーション ---
$traceId = bin2hex(random_bytes(16)); // リクエストの開始時に発行

function processOrder(string $traceId): void
{
    $logger = new TracedLogger('order-service', $traceId);

    $logger->span('order.validate', function () {
        usleep(10_000);
        return true;
    });

    $logger->span('order.payment', function () {
        usleep(50_000);
        // 決済サービスへ委譲(同じtraceIdを引き継ぐ)
        processPayment($traceId);
        return true;
    });

    $logger->info('order.completed', ['order_id' => 1001]);
}

function processPayment(string $traceId): void
{
    $logger = new TracedLogger('payment-service', $traceId);

    $logger->span('payment.charge', function () {
        usleep(30_000);
        return true;
    });
}

processOrder($traceId);
echo "Trace ID: {$traceId} で処理完了(journalctl -t order-service / payment-service で確認可能)\n";

出力イメージ(journalctl):

Jun 24 10:40:00 myserver order-service[12345]: {"trace_id":"a1b2...","service":"order-service","event":"order.validate.start",...}
Jun 24 10:40:00 myserver order-service[12345]: {"trace_id":"a1b2...","service":"order-service","event":"order.validate.success","duration_ms":10.2,...}
Jun 24 10:40:00 myserver payment-service[12345]: {"trace_id":"a1b2...","service":"payment-service","event":"payment.charge.start",...}

解説: 同じ trace_id を複数サービスのログに含めることで、journalctl や集約ログシステム(ELK、Lokiなど)で trace_id をキーに検索すれば、サービス間をまたいだ処理の全体像を追跡できます。


サンプル5:環境別のログ出力切り替え

開発環境ではファイル/標準出力、本番環境ではsyslogに出力を切り替えるパターンです。

<?php
declare(strict_types=1);

interface LoggerInterface
{
    public function log(int $priority, string $message, array $context = []): void;
}

class SyslogAdapter implements LoggerInterface
{
    public function __construct(string $ident, int $facility = LOG_USER)
    {
        openlog($ident, LOG_PID, $facility);
    }

    public function log(int $priority, string $message, array $context = []): void
    {
        $full = $context !== []
            ? $message . ' ' . json_encode($context, JSON_UNESCAPED_UNICODE)
            : $message;
        syslog($priority, $full);
    }

    public function __destruct()
    {
        closelog();
    }
}

class ConsoleAdapter implements LoggerInterface
{
    private const PRIORITY_LABELS = [
        LOG_EMERG   => 'EMERGENCY',
        LOG_ALERT   => 'ALERT',
        LOG_CRIT    => 'CRITICAL',
        LOG_ERR     => 'ERROR',
        LOG_WARNING => 'WARNING',
        LOG_NOTICE  => 'NOTICE',
        LOG_INFO    => 'INFO',
        LOG_DEBUG   => 'DEBUG',
    ];

    public function log(int $priority, string $message, array $context = []): void
    {
        $label = self::PRIORITY_LABELS[$priority] ?? 'UNKNOWN';
        $line  = sprintf('[%s] %s: %s', date('Y-m-d H:i:s'), $label, $message);
        if ($context !== []) {
            $line .= ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
        }
        echo $line . "\n";
    }
}

/**
 * 環境に応じてロガーを切り替えるファクトリ
 */
class LoggerFactory
{
    public static function create(string $env, string $ident): LoggerInterface
    {
        return match ($env) {
            'production' => new SyslogAdapter($ident, LOG_LOCAL0),
            default      => new ConsoleAdapter(),
        };
    }
}

// --- 使用例 ---
$env    = getenv('APP_ENV') ?: 'development';
$logger = LoggerFactory::create($env, 'myapp');

$logger->log(LOG_INFO, 'サーバー起動', ['env' => $env, 'pid' => getmypid()]);
$logger->log(LOG_WARNING, 'キャッシュミス率が上昇', ['rate' => 0.42]);
$logger->log(LOG_ERR, '外部APIエラー', ['endpoint' => '/v1/charge', 'status' => 502]);

実行結果(development環境):

[2026-06-24 10:45:00] INFO: サーバー起動 {"env":"development","pid":12345}
[2026-06-24 10:45:00] WARNING: キャッシュミス率が上昇 {"rate":0.42}
[2026-06-24 10:45:00] ERROR: 外部APIエラー {"endpoint":"\/v1\/charge","status":502}

解説: LoggerInterface で抽象化することで、開発時はコンソール出力、本番ではsyslogという切り替えがアプリケーションコードに影響を与えずに実現できます。実際のプロジェクトではMonologなどの成熟したライブラリの利用も検討すべきですが、軽量な自前実装としてはこの構成が有効です。


サンプル6:監視アラートとの連携(重要度フィルタリング)

特定の優先度以上のログのみを外部通知システムに転送するパターンです。

<?php
declare(strict_types=1);

class AlertingSyslogger
{
    public function __construct(
        private readonly string $ident,
        private readonly int $alertThreshold = LOG_ERR, // この優先度以下(数値が小さい=重要)でアラート
    ) {
        openlog($ident, LOG_PID, LOG_LOCAL0);
    }

    /**
     * ログを記録し、必要に応じてアラートを発火する
     */
    public function log(int $priority, string $message, array $context = []): void
    {
        $fullMessage = $context !== []
            ? $message . ' ' . json_encode($context, JSON_UNESCAPED_UNICODE)
            : $message;

        // syslogへの記録は常に行う
        syslog($priority, $fullMessage);

        // 重要度が閾値以上(数値的には以下)ならアラート発火
        if ($priority <= $this->alertThreshold) {
            $this->triggerAlert($priority, $message, $context);
        }
    }

    private function triggerAlert(int $priority, string $message, array $context): void
    {
        $priorityLabel = match ($priority) {
            LOG_EMERG => 'EMERGENCY',
            LOG_ALERT => 'ALERT',
            LOG_CRIT  => 'CRITICAL',
            LOG_ERR   => 'ERROR',
            default   => 'UNKNOWN',
        };

        // 実際の実装では Slack Webhook / PagerDuty / メール送信などを呼ぶ
        echo "🚨 [ALERT TRIGGERED] [{$priorityLabel}] {$message}\n";
        if ($context !== []) {
            echo "    詳細: " . json_encode($context, JSON_UNESCAPED_UNICODE) . "\n";
        }

        // syslogにも「アラート送信済み」の記録を残す
        syslog(LOG_NOTICE, "アラート通知を送信しました: {$message}");
    }

    public function __destruct()
    {
        closelog();
    }
}

// --- 使用例 ---
$logger = new AlertingSyslogger('myapp-critical', alertThreshold: LOG_ERR);

echo "=== 通常のINFOログ(アラートなし) ===\n";
$logger->log(LOG_INFO, 'ユーザーがログインしました', ['user_id' => 42]);

echo "\n=== WARNINGログ(アラートなし、閾値未満) ===\n";
$logger->log(LOG_WARNING, 'API応答が遅延', ['duration_ms' => 3200]);

echo "\n=== ERRORログ(アラート発火) ===\n";
$logger->log(LOG_ERR, 'データベース接続失敗', ['host' => 'db-primary', 'retry' => 3]);

echo "\n=== CRITICALログ(アラート発火) ===\n";
$logger->log(LOG_CRIT, '決済処理が完全に停止', ['affected_orders' => 15]);

実行結果:

=== 通常のINFOログ(アラートなし) ===

=== WARNINGログ(アラートなし、閾値未満) ===

=== ERRORログ(アラート発火) ===
🚨 [ALERT TRIGGERED] [ERROR] データベース接続失敗
    詳細: {"host":"db-primary","retry":3}

=== CRITICALログ(アラート発火) ===
🚨 [ALERT TRIGGERED] [CRITICAL] 致命的エラー
    詳細: {"affected_orders":15}

解説: 優先度の数値比較(<=)により「ERROR以上の重大度」だけをフィルタリングしてアラート処理を発火させています。実運用ではこの部分をSlack通知やPagerDuty APIへの送信に置き換えることで、重要なエラーだけを即座に検知できる仕組みが作れます。


よくある落とし穴

① openlog() を呼ばずに syslog() だけ使う

// ❌ openlog()なしでも動作するが、識別子(ident)が不明確になる
syslog(LOG_INFO, 'メッセージ');

// ✅ openlog()で明確な識別子を設定する
openlog('myapp', LOG_PID, LOG_USER);
syslog(LOG_INFO, 'メッセージ');
closelog();

② closelog() を忘れる

// ❌ 開いたままにすると、後続のsyslog呼び出しで意図しない識別子が使われる可能性
openlog('service-a', LOG_PID, LOG_USER);
syslog(LOG_INFO, 'A起動');
// closelog()忘れ

openlog('service-b', LOG_PID, LOG_USER);
syslog(LOG_INFO, 'B起動'); // 環境によってはservice-aの設定が影響する場合がある

// ✅ 必ず閉じる
openlog('service-a', LOG_PID, LOG_USER);
syslog(LOG_INFO, 'A起動');
closelog();

③ 改行コードを含むメッセージ

// ❌ syslogは1行のメッセージを前提とすることが多く、改行が含まれると表示が崩れる
syslog(LOG_INFO, "1行目\n2行目\n3行目");

// ✅ 改行を除去するか、別々のsyslog呼び出しに分ける
syslog(LOG_INFO, str_replace("\n", ' | ', "1行目\n2行目\n3行目"));

④ 機密情報をログに含める

// ❌ syslogはシステム共有のログであり、他のプロセス/管理者からも見える
syslog(LOG_INFO, "ログイン: password={$password}"); // 絶対NG

// ✅ 機密情報はマスクするか出力しない
syslog(LOG_INFO, "ログイン成功: user_id={$userId}");

⑤ Windowsでの挙動の違い

// Windowsではsyslog()はイベントログに書き込まれる
// ただしファシリティの概念がUnixと異なるため、移植性を考慮する場合は注意
if (PHP_OS_FAMILY === 'Windows') {
    // イベントログとしての挙動を確認しておく
}

まとめ

ポイント内容
主な用途システムログ(syslog/journalctl、Windowsイベントログ)へのメッセージ送信
セット関数openlog()で接続を開き、syslog()で送信、closelog()で閉じる
優先度LOG_EMERG(0) ~ LOG_DEBUG(7)。数値が小さいほど重要
ファシリティLOG_USERLOG_LOCAL07などでカテゴリ分け
活用パターン構造化ログ・例外ハンドラ統合・トレースID連携・アラート連携
注意点改行非対応・機密情報の混入・closelog()忘れ

syslog() はOS標準のログ機構と連携できる強力な関数です。独自のログファイル管理を行わずに、journalctl や既存の集約ログシステム(ELK、Lokiなど)にそのまま乗せられる点が大きな利点です。優先度とファシリティを適切に設計し、本記事のパターンを応用することで、運用に強いログ設計が実現できます。


PHP 8.x / 執筆時点の最新安定版にて動作確認済み

タイトルとURLをコピーしました