Summary: In This article, let us explore the PHP language to build a basic server-side monitoring engine of the many skills and considerations, and give a complete source code implementation.
I. Changes to the working directory
When you write a watchdog, it's usually better to set up your own working directory. In this way, if you use a relative path to read and write files, it automatically handles the location of the user's desired file placement. Always restricting the path used in a program is a good practice, but it loses the flexibility it deserves. Therefore, the safest way to change your working directory is to use both the chdir () and the Chroot ().
Chroot () is available in the CLI and CGI versions of PHP, but requires the program to run with root permissions. Chroot () actually changes the path of the current process from the root directory to the specified directory. This allows the current process to execute only files that exist in the directory. Often, chroot () is used by the server as a "security device" to ensure that malicious code does not modify files outside of a particular directory. Keep in mind that although chroot () can prevent you from accessing any files outside your new directory, any currently open file resources can still be accessed. For example, the following code can open a log file, call Chroot (), and switch to a data directory, and then still be able to log on successfully and then open the file resource:
<?php
$logfile = fopen ("/var/log/chroot.log", "w");
Chroot ("/users/george");
Fputs ($logfile, "Hello from Inside the chroot\n");
? >
If an application cannot use Chroot (), then you can call ChDir () to set up the working directory. This is useful, for example, when the code needs to load specific code that can be positioned anywhere in the system. Note that chdir () does not provide a security mechanism to prevent the opening of unauthorized files.
Two. Waiver of Privileges
A classic security precaution when writing a UNIX daemon is to let them discard all the privileges they do not need; otherwise, having unwanted privileges can easily incur unnecessary hassle. In cases where the code (or PHP itself) contains vulnerabilities, it is often possible to minimize the loss by ensuring that a daemon is run as the least privileged user.
One way to do this is to execute the daemon as a non privileged user. However, this is usually not sufficient if the program needs to open resources (such as log files, data files, sockets, and so on) that are not open to the unprivileged user in the first instance.
If you are running as a root user, you can use the Posix_setuid () and Posiz_setgid () functions to give up your privileges. The following example changes the privileges of the currently running program to those that are owned by the user nobody:
$PW =posix_getpwnam (' nobody ');
Posix_setuid ($PW [' uid ']);
Posix_setgid ($PW [' gid ']);
Just like chroot (), any privileged resources opened before the privilege is discarded will remain open, but new resources cannot be created.
three. Ensure exclusive sex
You may often want to implement a script that runs only one instance at any one time. This is especially important in order to protect the script, because running in the background can easily lead to multiple instances being invoked by chance.
The standard technique to ensure this exclusive is to have the script lock a particular file (often a lock file, and be used as a flock) by using the (). If the lock fails, the script should output an error and exit. Here is an example:
$FP =fopen ("/tmp/.lockfile", "a");
if (! $fp | |!flock ($FP, LOCK_EX | LOCK_NB)) {
Fputs (STDERR, "Failed to acquire lock\n");
Exit
}
/* Successfully locked to perform work safely
Note that the discussion of the lock mechanism involves more content and is not explained here.
Four. Building Monitoring Services
In this section, we'll use PHP to write a basic monitoring engine. Because you don't know how to change in advance, you should make it more flexible and possible.
The logger should be able to support arbitrary service checks (for example, HTTP and FTP services) and can log events in any way (via email, output to a log file, and so on). Of course you want it to run as a daemon, so you should ask it to output its full current state.
A service needs to implement the following abstract classes:
Abstract class Servicecheck {
Const FAILURE = 0;
Const SUCCESS = 1;
protected $timeout = 30;
protected $next _attempt;
protected $current _status = servicecheck::success;
protected $previous _status = servicecheck::success;
protected $frequency = 30;
protected $description;
protected $consecutive _failures = 0;
protected $status _time;
protected $failure _time;
Protected $loggers = Array ();
Abstract public Function __construct ($params);
Public Function __call ($name, $args)
{
if (Isset ($this-> $name)) {
return $this-> $name;
}
}
Public Function set_next_attempt ()
{
$this->next_attempt = time () + $this->frequency;
}
Public abstract function run ();
Public Function Post_run ($status)
{
if ($status!== $this->current_status) {
$this->previous_status = $this->current_status;
}
if ($status = = self::failure) {
if ($this->current_status = = self::failure) {
$this->consecutive_failures++;
}
else {
$this->failure_time = time ();
}
}
else {
$this->consecutive_failures = 0;
}
$this->status_time = time ();
$this->current_status = $status;
$this->log_service_event ();
}
Public Function Log_current_status ()
{
foreach ($this->loggers as $logger) {
$logger->log_current_status ($this);
}
}
Private Function Log_service_event ()
{
foreach ($this->loggers as $logger) {
$logger->log_service_event ($this);
}
}
Public Function Register_logger (Servicelogger $logger)
{
$this->loggers[] = $logger;
}
}
The __call () overload method above provides read-only access to the parameters of a Servicecheck object:
· timeout-How long the check can be suspended before the engine terminates the check.
· Next_attempt-the next time you try to connect to the server.
· The current status of the current_status-service: Success or failure.
· Previous_status-the state before the current state.
· frequency-Check the service every how often.
· description-Service description.
· consecutive_failures-the number of consecutive failed service checks since the last success.
· The last time the status_time-service was checked.
· failure_time-If the status is failed, it represents the time that the failure occurred.
This class also implements the observer pattern, allowing an object of type Servicelogger to register itself, and then call it when Log_current_status () or log_service_event () is invoked.
The key function implemented here is run (), which is responsible for defining how the check should be performed. If the check succeeds, it should return success; otherwise return failure.
The Post_run () method is invoked when the service check that is defined in run () is returned. It is responsible for setting the state of the object and implementing logging.
Servicelogger interface: Specifying a log class requires only two methods: Log_service_event () and Log_current_status (), which are invoked when a run () check is returned and when a normal state request is implemented.
The interface looks like this:
Interface Servicelogger {
Public Function log_service_event (servicecheck$service);
Public Function Log_current_status (servicecheck$service);
}
Finally, you need to write the engine itself. The idea is similar to the idea used when writing a simple program in the previous section: the server should create a new process to process each check and use a SIGCHLD processor to detect the return value when the check is complete. The maximum number that can be checked at the same time should be configurable to prevent the transition to system resources. All services and logs will be defined in an XML file.
The following is the Servicecheckrunner class that defines the engine:
Class Servicecheckrunner {
Private $num _children;
Private $services = Array ();
Private $children = Array ();
Public Function _ _construct ($conf, $num _children)
{
$loggers = Array ();
$this->num_children = $num _children;
$conf = Simplexml_load_file ($conf);
foreach ($conf->loggers->logger as $logger) {
$class = new Reflection_class ("$logger->class");
if ($class->isinstantiable ()) {
$loggers ["$logger->id"] = $class->newinstance ();
}
else {
Fputs (STDERR, "{$logger->class} cannot be instantiated.\n");
Exit
}
}
foreach ($conf->services->service as $service) {
$class = new Reflection_class ("$service->class");
if ($class->isinstantiable ()) {
$item = $class->newinstance ($service->params);
foreach ($service->loggers->logger as $logger) {
$item->register_logger ($loggers ["$logger"]);
}
$this->services[] = $item;
}
else {
Fputs (STDERR, "{$service->class} is not instantiable.\n");
Exit
}
}
}
Private function Next_attempt_sort ($a, $b) {
if ($a->next_attempt () = = $b->next_attempt ()) {
return 0;
}
Return ($a->next_attempt () $b->next_attempt ())? -1:1;
}
Private function Next () {
Usort ($this->services,array ($this, ' next_attempt_sort '));
return $this->services[0];
}
Public Function loop () {
Declare (Ticks=1);
Pcntl_signal (SIGCHLD, Array ($this, "Sig_child"));
Pcntl_signal (SIGUSR1, Array ($this, "SIG_USR1"));
while (1) {
$now = time ();
if (count ($this->children) $this->num_children) {
$service = $this->next ();
if ($now $service->next_attempt ()) {
Sleep (1);
Continue
}
$service->set_next_attempt ();
if ($pid = Pcntl_fork ()) {
$this->children[$pid] = $service;
}
else {
Pcntl_alarm ($service->timeout ());
Exit ($service->run ());
}
}
}
}
Public Function Log_current_status () {
foreach ($this->services as $service) {
$service->log_current_status ();
}
}
Private Function Sig_child ($signal) {
$status = servicecheck::failure;
Pcntl_signal (SIGCHLD, Array ($this, "Sig_child"));
while (($pid = pcntl_wait ($status, Wnohang)) > 0) {
$service = $this->children[$pid];
unset ($this->children[$pid]);
if (pcntl_wifexited ($status) && pcntl_wexitstatus ($status) ==servicecheck::success)
{
$status = servicecheck::success;
}
$service->post_run ($status);
}
}
Private Function SIG_USR1 ($signal) {
Pcntl_signal (SIGUSR1, Array ($this, "SIG_USR1"));
$this->log_current_status ();
}
}
This is a very complex class. Its constructor reads and analyzes an XML file, creates all the services that will be monitored, and creates a log program that records them.
The loop () method is the primary method in this class. It sets the requested signal processor and checks to see if a new child process can be created. Now, if the next event (sorted in next_attempt time Chuo) works well, a new process will be created. In this new subprocess, a warning is issued to prevent the test duration from exceeding its time limit and then executing the tests defined by run ().
There are also two signal processors: the SIGCHLD processor Sig_child (), which collects the aborted subprocess and executes the Post_run () method of their service, SIGUSR1 processor SIG_USR1 (), and simply invokes the log_ of all registered log programs Current_status () method, which can be used to get the current state of the entire system.
Of course, this monitoring architecture is not doing anything practical. But first, you need to check a service. The following class checks whether you retrieve a "server OK" response from an HTTP server:
Class Http_servicecheck extends servicecheck{
public $url;
Public Function _ _construct ($params) {
foreach ($params as $k => $v) {
$k = "$k";
$this-> $k = "$v";
}
}
Public Function run () {
if (Is_resource (@fopen ($this->url, "R")) {
return servicecheck::success;
}
else {
return servicecheck::failure;
}
}
}
This is a very simple service compared to the framework you've built before, and it's not much to describe here.
Five. Sample Servicelogger process
The following is an example Servicelogger process. When a service is deactivated, it is responsible for sending an e-mail message to a standby person:
Class Emailme_servicelogger implements Servicelogger {
Public Function log_service_event (servicecheck$service)
{
if ($service->current_status ==servicecheck::failure) {
$message = "Problem with{$service->description ()}\r\n";
Mail (' oncall@example.com ', ' Service Event ', $message);
if ($service->consecutive_failures () 5) {
Mail (' oncall_backup@example.com ', ' Service Event ', $message);
}
}
}
Public Function Log_current_status (servicecheck$service) {
Return
}
}
If you fail five consecutive times, the process also sends a message to a backup address. Note that it does not implement a meaningful log_current_status () method.
Whenever you change the state of a service as follows, you should implement a Servicelogger process that writes to the PHP error log:
Class Errorlog_servicelogger implements Servicelogger {
Public Function log_service_event (servicecheck$service)
{
if ($service->current_status ()!== $service->previous_status ()) {
if ($service->current_status () ===servicecheck::failure) {
$status = ' down ';
}
else {
$status = ' up ';
}
Error_log ("{$service->description ()} changed status to $status");
}
}
Public Function Log_current_status (servicecheck$service)
{
Error_log ("{$service->description ()}: $status");
}
}
The Log_current_status () method means that if the process sends a SIGUSR1 signal, it will copy its full current state to your PHP error log.
The engine uses one of the following configuration files:
<config>
<loggers>
<logger>
<id> errorlog </id>
<class> Errorlog_servicelogger </class>
</logger>
<logger>
<id> Emailme </id>
<class> Emailme_servicelogger </class>
</logger>
</loggers>
<services>
<service>
<class> Http_servicecheck </class>
<params>
<description> Omniti HTTP Check </description>
<url> http://www.omniti.com </url>
<timeout> </timeout>
<frequency> 900 </frequency>
</params>
<loggers>
<logger> errorlog </logger>
<logger> Emailme </logger>
</loggers>
</service>
<service>
<class> Http_servicecheck </class>
<params>
<description> Home Page HTTP Check </description>
<url> Http://www.schlossnagle.org/~george </url>
<timeout> </timeout>
<frequency> 3600 </frequency>
</params>
<loggers>
<logger> errorlog </logger>
</loggers>
</service>
</services>
</config>
When this XML file is passed, the Servicecheckrunner constructor instantiates a logger for each specified log. It then instantiates a Servicecheck object corresponding to each specified service.
Note that the constructor uses the Reflection_class class to implement internal checks for the service and log classes-before you attempt to instantiate them. Although this is not necessary, it is a good illustration of the use of the new Reflection (Reflection) API in PHP 5. In addition to these classes, the reflection API provides classes to implement internal checks on almost any internal entity (class, method, or function) in PHP.
In order to use the engine you built, you still need some packaging code. The watchdog should prevent you from trying to start it two times-you don't need to create two messages for each event. Of course, the watchdog should also receive options that include the following options:
Options |
Describe |
[F] |
A location of the engine's configuration file, the default is Monitor.xml. |
[-N] |
The size of the child process pool allowed by the engine, default is 5. |
[d] |
A flag that disables the daemon function of the engine. This is useful when you are writing a debug Servicelogger process that outputs information to stdout or stderr. |
The following is the final watchdog script, which analyzes options, guarantees exclusive and runs service checks:
Require_once "Service.inc";
Require_once "console/getopt.php";
$shortoptions = "N:f:d";
$default _opts = Array (' n ' => 5, ' F ' => ' monitor.xml ');
$args = getoptions ($default _opts, $shortoptions, NULL);
$fp = fopen ("/tmp/.lockfile", "a");
if (! $fp | |!flock ($FP, LOCK_EX | LOCK_NB)) {
Fputs ($stderr, "Failed to acquire lock\n");
Exit
}
if (! $args [' d ']) {
if (Pcntl_fork ()) {
Exit
}
Posix_setsid ();
if (Pcntl_fork ()) {
Exit
}
}
Fwrite ($FP, Getmypid ());
Fflush ($FP);
$engine = new Servicecheckrunner ($args [' F '], $args [' n ']);
$engine->loop ();
Note that this example uses the custom GetOptions () function.
After you write an appropriate configuration file, you can start the script as follows:
>/monitor.php-f/etc/monitor.xml
This can protect and continue monitoring until the machine is turned off or the script is killed.
The script is quite complex, but there are still some areas that are easy to improve, which have to be left to the reader for practice:
· Add a sighup processor that analyzes the configuration file so that you can change the configuration without starting the server.
· Write a servicelogger that can log in to a database to store query data.
· Write a Web front-end program that provides a good GUI for the entire monitoring system.