Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Syslog
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
6 / 6
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getFacility
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHostname
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 writeLog
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 __destruct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Log\Writer;
16
17use Pop\Log\Facility;
18use Pop\Log\Level;
19
20/**
21 * Syslog log writer class
22 *
23 * Builds a real RFC-3164 packet (<PRI>HEADER TAG: MSG) and sends it over UDP to a syslog
24 * daemon/collector. Does not use Logger's configurable timestampFormat — RFC-3164 mandates
25 * an exact HEADER timestamp shape, generated independently on every write.
26 *
27 * @category   Pop
28 * @package    Pop\Log
29 * @author     Nick Sagona, III <dev@noladev.com>
30 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
31 * @license    https://www.popphp.org/license     New BSD License
32 * @version    5.0.0
33 */
34class Syslog extends AbstractWriter
35{
36
37    /**
38     * UDP socket resource
39     * @var mixed
40     */
41    protected mixed $socket = null;
42
43    /**
44     * Tag (program identifier) written into the syslog TAG field
45     * @var string
46     */
47    protected string $tag;
48
49    /**
50     * Hostname written into the syslog HEADER
51     * @var string
52     */
53    protected string $hostname;
54
55    /**
56     * Constructor
57     *
58     * Instantiate the syslog writer object
59     *
60     * @param  Facility $facility
61     * @param  string   $host
62     * @param  int      $port
63     * @param  ?string  $tag
64     * @param  ?string  $hostname
65     * @param  bool     $includePid
66     * @throws Exception
67     */
68    public function __construct(
69        protected Facility $facility   = Facility::USER,
70        protected string   $host       = '127.0.0.1',
71        protected int      $port       = 514,
72        ?string             $tag        = null,
73        ?string             $hostname   = null,
74        protected bool      $includePid = true
75    ) {
76        $this->tag      = $tag ?? basename($_SERVER['SCRIPT_NAME'] ?? $_SERVER['argv'][0] ?? 'php');
77        $this->hostname = $hostname ?? (gethostname() ?: 'localhost');
78
79        // RFC-3164 4.1.2/4.1.3: HOSTNAME must not contain whitespace, TAG must be <= 32 chars with no whitespace/control chars
80        $this->tag      = substr(preg_replace('/[\s\x00-\x1F\x7F]+/', '', (string)$this->tag), 0, 32);
81        $this->hostname = preg_replace('/[\s\x00-\x1F\x7F]+/', '', (string)$this->hostname);
82
83        $socket = @stream_socket_client('udp://' . $this->host . ':' . $this->port, $errno, $errstr);
84
85        if ($socket === false) {
86            throw new Exception('Unable to open UDP socket to ' . $this->host . ':' . $this->port . ': ' . $errstr);
87        }
88
89        $this->socket = $socket;
90    }
91
92    /**
93     * Get facility
94     * @return Facility
95     */
96    public function getFacility(): Facility
97    {
98        return $this->facility;
99    }
100
101    /**
102     * Get tag
103     * @return string
104     */
105    public function getTag(): string
106    {
107        return $this->tag;
108    }
109
110    /**
111     * Get hostname
112     * @return string
113     */
114    public function getHostname(): string
115    {
116        return $this->hostname;
117    }
118
119    /**
120     * Write to the log
121     *
122     * @param  string $level
123     * @param  string $message
124     * @param  array  $context
125     * @return Syslog
126     */
127    public function writeLog(string $level, string $message, array $context = []): Syslog
128    {
129        if ($this->isWithinLogLimit($level)) {
130            $pri = ($this->facility->value * 8) + Level::toSeverity($level);
131
132            // RFC-3164 HEADER timestamp: "Mmm dd hh:mm:ss" - day is space-padded, not zero-padded
133            $now       = time();
134            $timestamp = sprintf('%s %2d %s', date('M', $now), (int)date('j', $now), date('H:i:s', $now));
135
136            $tagPart = $this->tag . ($this->includePid ? '[' . getmypid() . ']' : '');
137            $message = $this->sanitize($message);
138            $ctx     = $this->sanitize($this->getContext($context));
139            $body    = $message . ($ctx !== '' ? ' ' . $ctx : '');
140
141            $packet = '<' . $pri . '>' . $timestamp . ' ' . $this->hostname . ' ' . $tagPart . ': ' . $body;
142
143            if (strlen($packet) > 1024) {
144                $packet = mb_strcut($packet, 0, 1024);
145            }
146
147            fwrite($this->socket, $packet);
148        }
149
150        return $this;
151    }
152
153    /**
154     * Destructor
155     */
156    public function __destruct()
157    {
158        if (is_resource($this->socket)) {
159            fclose($this->socket);
160        }
161    }
162
163}