Build a simple monitoring engine using PHP

Source: Internet
Author: User
Tags flock php error log
? Source: dev.yesky.comweb2632638263.shtml Abstract: In this article, let's discuss a lot of tips and precautions for building a basic server Monitoring Engine Based on the PHP language, and provide a complete source code implementation. I. Change the working directory. When you write a Monitoring Program, let it set its own working directory.

? Http://dev.yesky.com/web/263/2638263.shtml Abstract: In this article, let's discuss a lot of skills and precautions for building a basic server monitoring engine based on PHP language, and give the complete source code implementation. I. Change the working directory. When you write a Monitoring Program, let it set its own working directory.

?

Source: http://dev.yesky.com/web/263/2638263.shtml

Summary: In this article, let's discuss a lot of tips and precautions for building a basic server Monitoring Engine Based on the PHP language, and provide the complete source code implementation.

  I. Change the working directory

When you write a monitoring program, it is usually better to set your working directory. In this way, if you use a relative path to read and write files, it will automatically process the location where the user expects to store the files as needed. It is always a good practice to restrict the paths used in the program, but it loses the necessary flexibility. Therefore, the safest way to change your working directory is to use both chdir () and chroot ().

Chroot () can be used in 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 only execute files in this directory. Usually, chroot () is used by the server as a "security device" to ensure that malicious code does not modify files outside a specific directory. Keep in mind that although chroot () can prevent you from accessing any files outside your new directory, any currently opened file resources can still be accessed. For example, the following code can open a log file, call chroot (), and switch to a data directory. Then, you can log on successfully and then open the file resources:

<? 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 (), you can call chdir () to set the working directory. For example, it is useful to load specific code (which can be located anywhere in the system. Note that chdir () does not provide a security mechanism to prevent unauthorized files from being opened.

   2. Give up privileges

When writing Unix daemon, a classic security precaution is to give up all unnecessary privileges; otherwise, having unnecessary privileges is easy to cause unnecessary trouble. When the code (or PHP itself) contains vulnerabilities, You can minimize the loss by ensuring that a daemon runs as a minimum-privilege user.

One way to achieve this is to execute the daemon as a non-privileged user. However, if the program needs to open resources (such as log files, data files, sockets, and so on) that are not authorized by the non-privileged user at the beginning, this is usually not enough.

If you run as the root user, you can discard your privileges by using the posix_setuid () and posiz_setgid () functions. The following example changes the privileges of the current running program to those permissions owned by the user nobody:

$ Pw = posix_getpwnam ('nobody ');
Posix_setuid ($ pw ['uid']);
Posix_setgid ($ pw ['gid']);

Like chroot (), any privileged resource opened before giving up the privilege will remain open, but cannot be created.

   3. Ensure exclusion

You may often want to achieve this: a script runs only one instance at any time. This is especially important to protect scripts because multiple instances can be called by chance when running in the background.

The standard technique to ensure this exclusion is to use flock () to lock the script to a specific file (often a locked file and used in the layout ). If the lock fails, the script should output an error and exit. The following is an example:

$ Fp = fopen ("/tmp/. lockfile", "");
If (! $ Fp |! Flock ($ fp, LOCK_EX | LOCK_NB )){
Fputs (STDERR, "Failed to acquire lock/n ");
Exit;
}
/* The task is locked to perform the task safely */

Note that the discussion about the lock mechanism involves a lot of content, so we will not explain it here.

?

4. Build a Monitoring Service

In this section, we will use PHP to compile a basic monitoring engine. Because you don't know how to change it in advance, you should make its implementation flexible and possible.
This record program should be able to support any service check (for example, HTTP and FTP services) and be able to record events in any way (via email, output to a log file, and so on. Of course you want it to run as a daemon; therefore, you should request it to output its complete current state.

A service must 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 $ previus_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-> previus_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 above _ call () overload method provides read-only access to the parameters of a ServiceCheck object:

· Timeout-how long the check can be suspended before the engine termination check.

· Next_attempt-the next attempt to connect to the server.

· Current_status-Current Status of the Service: SUCCESS or FAILURE.

· Previus_status-the status before the current status.

· Frequency-check the service at intervals.

· Description-service description.

· Consecutive_failures-the number of consecutive failed service checks since the previous success.

· Status_time-the last time when the service is checked.

· Failure_time-if the status is FAILED, it indicates the time when the failure occurred.

This class also implements the observer mode, allowing the ServiceLogger type objects to register themselves, and then calls it when calling log_current_status () or log_service_event.

The key function implemented here is run (), which defines how to perform the check. If the check succeeds, it should return SUCCESS; otherwise, return FAILURE.

The post_run () method is called when the service check returned in run () is defined. It is responsible for setting the object status and logging it to the log.

ServiceLogger interface: to specify a log class, you only need to implement two methods: log_service_event () and log_current_status () it is called when a normal State request is implemented during the check return time.

This interface is as follows:

Interface ServiceLogger {
Public function log_service_event (ServiceCheck $ service );
Public function log_current_status (ServiceCheck $ service );
}

Finally, you need to write the engine itself. This 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 of checks that can be performed at the same time should be configurable, so as to prevent the transitional use of system resources. All services and logs are defined in an XML file.

The ServiceCheckRunner class that defines the engine is as follows:

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 to be monitored, and creates the log programs that record them.

The loop () method is the main method in this class. It sets the requesting signal processor and checks whether a new sub-process can be created. Now, if the next event (sorted by next_attempt time CHUO) runs well, a new process will be created. In this new sub-process, issue a warning to prevent the test duration from exceeding its time limit and then execute the test defined by run.

There are also two signal processors: SIGCHLD processor sig_child (), which collects terminated sub-processes and executes the post_run () method of their services; SIGUSR1 processor sig_usr1 (), you can call the log_current_status () method of all registered log programs to obtain the current status of the entire system.

Of course, this monitoring architecture does not do anything practical. But first, you need to check a service. The following class checks whether you retrieve a "200 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 service is extremely simple compared to the framework you have previously built and will not be described here.

5. ServiceLogger process example

The following is an example ServiceLogger process. When a service is stopped, it is responsible for sending an email 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 five consecutive failures occur, 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 status of a service as follows, you should implement a ServiceLogger process written to the PHP Error Log:

Class ErrorLog_ServiceLogger implements ServiceLogger {
Public function log_service_event (ServiceCheck $ service)
{
If ($ service-> current_status ()! ==$ Service-> previus_status ()){
If ($ service-> current_status () === ServiceCheck: FAILURE ){
$ Status = 'low ';
}
Else {
$ Status = 'up ';
}
Error_log ("{$ service-> description ()} changed status to $ status ");
}
}
Public function log_current_status (ServiceCheck $ service)
{
Error_log ("{$ service-> description ()}: $ status ");
}
}

This log_current_status () method means that if a process sends a SIGUSR1 signal, it will copy its complete current status to your PHP error log.
  
The engine uses the following configuration file:

<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> 30 </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> 30 </timeout>
<Frequency> 3600 </frequency>
</Params>
<Loggers>
<Logger> errorlog </logger>
</Loggers>
</Service>
</Services>
</Config>

When this XML file is passed, the ServiceCheckRunner constructor instantiates a logging program for each specified log. Then, it instantiates a ServiceCheck object for each specified service.

Note that the constructor uses the Reflection_Class class to implement internal checks for the service and log classes-before you try to instantiate them. Although this is unnecessary, it demonstrates the use of the new Reflection API in PHP 5. In addition to these classes, the reflection API also provides classes to implement internal checks on almost any internal entities (classes, methods, or functions) in PHP.

To use the engine you built, you still need some packaging code. The monitoring program should forbid you from trying to start it twice-you do not need to create two messages for each event. Of course, this monitoring program should also receive the following options:

Option Description
[-F] The default location of the engine configuration file is monitor. xml.
[-N] The size of the sub-process pool allowed by the engine. The default value is 5.
[-D] A sign that disables the daemon function of the engine. This is useful when you compile a ServiceLogger process that outputs information to stdout or stderr for debugging.

The following is the final monitoring program script, which analyzes the options to ensure consistency and run the service check:

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", "");
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: This example uses the custom getOptions () function.

After compiling an appropriate configuration file, you can start the script as follows:

>./Monitor. php-f/etc/monitor. xml

This protects and continues monitoring until the machine is turned off or the script is killed.

This script is quite complex, but there are still some areas that are easy to improve, so it is left for the readers to use as exercises:

· Add a SIGHUP processor to re-analyze the configuration file so that you can change the configuration without starting the server.

· Compile a ServiceLogger that can log on to a database to store and query data.

· Compile a Web Front-End program to provide a good GUI for the entire monitoring system.

?

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.