coralogix/php-coralogix-sdk

View on GitHub
src/Coralogix/LoggerManager.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * Coralogix logger logs manager
 *
 * @author Eldar Aliiev <eldar@coralogix.com>
 * @link https://coralogix.com/
 * @copyright Coralogix Ltd. 2018
 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
 * @version 1.0.0
 * @since 1.0.0
 */

declare(strict_types=1);

namespace Coralogix;


use Coralogix\Constants;
use Coralogix\Severity;
use Coralogix\DebugLogger;
use Coralogix\Helpers;


/**
 * Class LoggerManager
 * @package Coralogix\Logger
 * @property bool $configured is logger manager configured
 * @property bool $sync_time synchronize local time with Coralogix servers
 * @property integer $time_delta_last_update time of last synchronization with Coralogix servers
 * @property integer $time_delta difference between local time and Coralogix servers
 * @property integer $buffer_size size of logs buffer in bytes
 * @property array $buffer logs buffer
 * @property bool $stopped is current logger manager stopped manually
 * @property array $bulk_template template of logs bulk
 * @final
 */
final class LoggerManager extends HttpSender
{
    /**
     * @var bool is logger manager configured
     * @access public
     */
    public $configured;

    /**
     * @var bool synchronize local time with Coralogix servers
     * @access private
     */
    private $sync_time;

    /**
     * @var integer time of last synchronization with Coralogix servers
     * @access private
     */
    private $time_delta_last_update;

    /**
     * @var integer difference between local time and Coralogix servers
     * @access private
     */
    private $time_delta;

    /**
     * @var integer size of logs buffer in bytes
     * @access private
     */
    private $buffer_size;

    /**
     * @var array logs buffer
     * @access private
     */
    private $buffer;

    /**
     * @var bool is current logger manager stopped manually
     * @access private
     */
    private $stopped;

    /**
     * @var array template of logs bulk
     * @access private
     */
    private $bulk_template;

    /**
     * Initialize logger manager instance
     * @param bool $sync_time synchronize local time with Coralogix servers
     * @param array $params configuration parameters(privateKey, applicationName, subsystemName)
     * @access public
     */
    public function __construct(array $params, bool $sync_time = true)
    {
        try {
            // Initialize parameters
            $this->configured = false;
            $this->sync_time = (bool)$sync_time;
            $this->time_delta_last_update = 0;
            $this->time_delta = 0;
            $this->buffer_size = 0;
            $this->buffer = array();
            $this->stopped = false;

            // Fill template of logs bulk with default values
            $this->bulk_template = array(
                "privateKey" => Constants::FAILED_PRIVATE_KEY,
                "applicationName" => Constants::NO_APP_NAME,
                "subsystemName" => Constants::NO_SUB_SYSTEM
            );

            // Update template of logs bulk with user parameters
            $this->bulk_template->merge(
                array(
                    "computerName" => Helpers::get_computer_name(),
                    "IPAddress" => Helpers::get_computer_address()
                )
            );

            // Check logger parameters
            if(!array_key_exists("privateKey", $params) || !is_string($params["privateKey"])) {
                throw new \Exception("Invalid private key");
            }
            $this->bulk_template->merge($params);

            DebugLogger::info("Successfully configured Coralogix logger");

            // Change logger manager configured status to success
            $this->configured = true;

            // Send initialize message to Coralogix
            $this->send_init_message();
        } catch (\Exception $e) {
            if (!$this->stopped) {
                DebugLogger::exception("Failed to configure Coralogix logger", $e);

                // Change logger manager configured status to fails
                $this->configured = false;
            }
        }
    }

    /**
     * Stop logger manager on exit
     * @access public
     */
    public function __destruct()
    {
        $this->stop();
    }

    /**
     * Target function for thread in which executing logs sending process
     * @access public
     */
    public function run()
    {
        try {
            // Execute while thread not stopped
            while (true) {
                if ($this->stopped) {
                    // Flush buffer before exit
                    $this->flush();
                    return;
                }

                // Send logs bulk
                $this->send_bulk($this->sync_time);

                // Change logs sending interval
                if ($this->buffer_size > (Constants::MAX_LOG_CHUNK_SIZE / 2)) {
                    $next_check_interval = Constants::FAST_SEND_SPEED_INTERVAL;
                } else {
                    $next_check_interval = Constants::NORMAL_SEND_SPEED_INTERVAL;
                }

                DebugLogger::debug("Next buffer check is scheduled in $next_check_interval seconds");

                // Sleep before next sending
                sleep((int)$next_check_interval);
            }
        } catch (\Exception $e) {
            if (!$this->stopped) {
                DebugLogger::exception("Exception from the main buffer loop", $e);
            }
        }
    }

    /**
     * Stop logger manager
     * @access public
     */
    public function stop()
    {
        $this->stopped = true;
    }

    /**
     * Send all logs to Coralogix before exit
     * @access public
     */
    public function flush()
    {
        $this->send_bulk(false);
    }

    /**
     * Get size of logs buffer
     * @param bool $in_bytes return in bytes or in count of entries
     * @return int buffer size
     */
    public function get_buffer_size(bool $in_bytes = false): int
    {
        return $in_bytes ? $this->buffer_size : sizeof($this->buffer);
    }

    /**
     * Add logs line to logs buffer
     * @param string $message log record text
     * @param integer $severity log record level
     * @param string $category log record category
     * @param array $params additional log record fields(className, methodName, threadId)
     * @access public
     */
    public function add_logline($message, int $severity, string $category = NULL, array $params = array())
    {
        try {
            if ($this->buffer_size < Constants::MAX_LOG_BUFFER_SIZE) {
                // Validate message
                $message = ($message && !ctype_space((string)$message)) ? $this->msg2str($message) : "EMPTY_STRING";

                if ($severity < Severity::DEBUG || $severity > Severity::CRITICAL) {
                    $severity = Severity::DEBUG;
                }

                // Validate category
                $category = ($category && !ctype_space((string)$category)) ? $category : Constants::CORALOGIX_CATEGORY;

                // Combine a log-entry from the must parameters together with the optional one
                $new_entry = array_merge(
                    array(
                        "text" => $message,
                        "timestamp" => time() * 1000 + $this->time_delta,
                        "severity" => $severity,
                        "category" => $category,
                    ),
                    $params
                );

                // Get new entry size
                $new_entry_size = strlen(json_encode($new_entry, JSON_UNESCAPED_UNICODE));

                // If log record bigger than maximal size throw error
                if (Constants::MAX_LOG_CHUNK_SIZE <= $new_entry_size) {
                    DebugLogger::warning(
                        sprintf(
                            "add_logline(): received log message too big of size= %d MB, bigger than max_log_chunk_size= %d; throwing...",
                            (int)($new_entry_size / 1024 ** 2),
                            Constants::MAX_LOG_CHUNK_SIZE
                        )
                    );
                    throw new \Exception("Log message is two large");
                }

                // Add log entry to logs buffer
                $this->buffer[] = $new_entry;

                // Update the buffer size to reflect the new size
                $this->buffer_size += $new_entry_size;

            }
        } catch (\Exception $e) {
            if (!$this->stopped) {
                DebugLogger::exception("Failed to add log to buffer", $e);
            }
        }
    }

    /**
     * Send initialize message to Coralogix at start of working
     * @access private
     */
    private function send_init_message()
    {
        // Send to Coralogix record with information about current logger
        $this->add_logline(
            sprintf(
                "The Application Name %s and Subsystem Name %s from the PHP SDK, version %s has started to send data",
                $this->bulk_template['applicationName'],
                $this->bulk_template['subsystemName'],
                Helpers::get_package_version()
            ),
            Severity::INFO,
            Constants::CORALOGIX_CATEGORY,
            array(
                "threadId" => (string)(\Thread::getCurrentThreadId())
            )
        );
    }

    /**
     * Send logs bulk to Coralogix
     * @param bool $time_sync synchronize local time with Coralogix servers
     * @access private
     */
    private function send_bulk(bool $time_sync = true)
    {
        try {
            // Check if current logger manager is configured
            if (!$this->configured) return;

            // Check if time synchronization required
            if ($time_sync) {
                $this->update_time_delta_interval();
            }

            // Total buffer size
            $size = $this->buffer->count();
            if ($size < 1) {
                DebugLogger::info("Buffer is empty, there is nothing to send!");
                return;
            }

            // If the size is bigger than the maximum allowed chunk size then split it by half.
            // Keep splitting it until the size is less than MAX_LOG_CHUNK_SIZE
            $buffer_copy = (array)$this->buffer;
            while ((strlen(json_encode(array_slice($buffer_copy, 0, $size), JSON_UNESCAPED_UNICODE)) > Constants::MAX_LOG_CHUNK_SIZE) && $size > 1) {
                $size = (int)($size / 2);
            }

            // We must take at least one value.
            // If the first message is bigger than MAX_LOG_CHUNK_SIZE
            // we need to take it anyway.
            $size = $size > 0 ? $size : 1;

            DebugLogger::info("Checking buffer size. Total log entries is: $size");

            // Get logs entries
            $bulk = (array)$this->bulk_template;
            for ($i = 0; $i < $size; $i++) {
                $bulk["logEntries"][] = (array)$this->buffer->pop();
            }

            // Extract from the buffer size the total amount of the logs we removed from the buffer
            $this->buffer_size -= (strlen(json_encode($bulk["logEntries"], JSON_UNESCAPED_UNICODE)) - $size * 2);

            // Make sure we are always positive
            $this->buffer_size = max($this->buffer_size, 0);

            DebugLogger::info("Buffer size after removal is: " . $this->buffer_size);

            // Sending bulk
            if ($bulk["logEntries"]) {
                self::send_request($bulk);
            }
        } catch (\Exception $e) {
            DebugLogger::exception("Failed to send bulk", $e);
        }
    }

    /**
     * Format message string to correct format
     * @param string $message log record text
     * @access public
     * @return string formatted string
     */
    public function msg2str($message): string
    {
        try {
            // Format message content according to it's type
            if (is_string($message)) {
                return $message;
            } else if (is_array($message) || is_object($message)) {
                return json_encode($message, JSON_UNESCAPED_UNICODE);
            } else if (is_null($message)) {
                return "";
            } else {
                throw new \Exception("Invalid format");
            }
        } catch (\Exception $e) {
            return (string)$message;
        }
    }

    /**
     * Get time difference between local machine and Coralogix servers
     * @access private
     */
    private function update_time_delta_interval()
    {
        try {
            if (time() - $this->time_delta_last_update >= 60 * Constants::SYNC_TIME_UPDATE_INTERVAL) {
                // Get time information from Coralogix servers
                list($result, $time_delta) = self::get_time_sync();

                // If information was got successfully - update time difference information
                if ($result) {
                    $this->time_delta = $time_delta;
                    $this->time_delta_last_update = time();
                } else {
                    // ... or throw exception
                    throw new \Exception("Time synchronization was unsuccessful");
                }
            }
        } catch (\Exception $e) {
            if (!$this->stopped) {
                DebugLogger::exception("Failed to update time sync", $e);
            }
        }
    }
}