はじめに
アプリケーションのログをどこに記録するかは、運用において重要な設計判断です。独自のログファイルに書き込む方法もありますが、OSが提供するシステムログ機構にメッセージを送る選択肢もあります。
syslog() は、PHPからUnix系OSの syslog デーモン(rsyslog・syslog-ngなど)やWindowsのイベントログにメッセージを送信する関数です。openlog()・closelog() と組み合わせて使うことで、優先度(重要度)やファシリティ(カテゴリ)を指定した構造化ログが実現できます。
本記事では関数の仕様から、実践的なログ設計パターンまで詳しく解説します。
関数の基本情報
| 項目 | 内容 |
|---|---|
| 関数名 | syslog() |
| 利用可能バージョン | PHP 4以降 |
| 所属 | システム関数(Misc Functions) |
| 戻り値 | bool(成功時true、失敗時false) |
| 拡張機能 | 不要(コア関数。Windowsではイベントログとして動作) |
シグネチャ
syslog(int $priority, string $message): bool
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$priority | int | ログの優先度(重要度)。LOG_* 定数を使用 |
$message | string | ログメッセージ本文 |
優先度(Priority)の一覧
syslogの優先度はUnixの標準仕様(RFC 5424)に基づいています。
| 定数 | 値 | 意味 | 使用場面の例 |
|---|---|---|---|
LOG_EMERG | 0 | システムが使用不能 | システム全体がダウン寸前 |
LOG_ALERT | 1 | 即時対応が必要 | データ破損の検出など |
LOG_CRIT | 2 | 致命的な状態 | DB接続が完全に失われた等 |
LOG_ERR | 3 | エラー | 処理失敗・例外発生 |
LOG_WARNING | 4 | 警告 | リトライ可能な異常・非推奨API使用 |
LOG_NOTICE | 5 | 通知(正常だが注目すべき) | 設定変更・特権操作 |
LOG_INFO | 6 | 情報 | 通常の処理ログ |
LOG_DEBUG | 7 | デバッグ情報 | 詳細なトレース情報 |
数値が小さいほど重要度が高い点に注意してください。
ファシリティ(Facility)の一覧
openlog() で指定する「どのカテゴリのログか」を示す値です。
| 定数 | 意味 |
|---|---|
LOG_USER | 一般的なユーザーレベルのメッセージ(デフォルト) |
LOG_LOCAL0〜LOG_LOCAL7 | カスタム用途(アプリケーション独自のカテゴリ分け) |
LOG_MAIL | メールシステム関連 |
LOG_DAEMON | システムデーモン関連 |
LOG_AUTH | 認証関連(セキュリティ) |
LOG_CRON | cron関連 |
Webアプリケーションでは
LOG_USERまたはLOG_LOCAL0〜LOG_LOCAL7を使うのが一般的です。
openlog() / closelog() との関係
openlog($ident, $option, $facility) → syslog接続を開く(識別子・オプション・ファシリティを設定)
syslog($priority, $message) → ログメッセージを送信(複数回呼べる)
closelog() → syslog接続を閉じる
openlog() のオプション定数
| 定数 | 意味 |
|---|---|
LOG_PID | メッセージにプロセスIDを含める |
LOG_CONS | syslogへの送信に失敗した場合、コンソールに出力 |
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_USER・LOG_LOCAL0〜7などでカテゴリ分け |
| 活用パターン | 構造化ログ・例外ハンドラ統合・トレースID連携・アラート連携 |
| 注意点 | 改行非対応・機密情報の混入・closelog()忘れ |
syslog() はOS標準のログ機構と連携できる強力な関数です。独自のログファイル管理を行わずに、journalctl や既存の集約ログシステム(ELK、Lokiなど)にそのまま乗せられる点が大きな利点です。優先度とファシリティを適切に設計し、本記事のパターンを応用することで、運用に強いログ設計が実現できます。
PHP 8.x / 執筆時点の最新安定版にて動作確認済み
