diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 1e294e5..388da64 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,15 +1,22 @@ checks: - php: - code_rating: true - duplication: true + php: true filter: excluded_paths: - - tests/* + - 'tests/*' build: - tests: - override: - - - command: ./vendor/bin/phpunit --coverage-clover=clover.xml - coverage: - file: clover.xml - format: php-clover + environment: + php: + version: 7.2 + ini: + 'date.timezone': 'Asia/Shanghai' + dependencies: + before: + - pecl install swoole-4.4.5 + - composer install + nodes: + analysis: + project_setup: + override: true + tests: + override: + - php-scrutinizer-run --enable-security-analysis \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 7a7a62f..24b57af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,44 @@ language: php -php: - - 7.1 - - 7.2 +sudo: required -before_script: - - pecl install SeasLog - - composer self-update - - composer install --prefer-source --no-interaction --dev +matrix: + include: + - php: 7.1 + env: SW_VERSION="4.4.5";SL_VERSION="SeasLog-2.0.2" + - php: 7.2 + env: SW_VERSION="4.4.5";SL_VERSION="SeasLog-2.0.2" + - php: 7.3 + env: SW_VERSION="4.4.5";SL_VERSION="SeasLog-2.0.2" + - php: master + env: SW_VERSION="4.4.5";SL_VERSION="SeasLog-2.0.2" -after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + allow_failures: + - php: master -script: ./vendor/bin/phpunit --coverage-clover=coverage.clover +services: + - mysql -matrix: - fast_finish: true +before_install: + - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)" + - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)" + - echo $PHP_MAJOR + - echo $PHP_MINOR + +install: + - cd $TRAVIS_BUILD_DIR + - bash .travis/swoole.install.sh + - bash .travis/seaslog.install.sh + - phpenv config-rm xdebug.ini || echo "xdebug not available" + - phpenv config-add .travis/ci.ini + +before_script: + - cd $TRAVIS_BUILD_DIR + - composer self-update + - composer install --prefer-source --no-interaction --dev + +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover +script: ./vendor/bin/phpunit --coverage-clover=coverage.clover \ No newline at end of file diff --git a/.travis/ci.ini b/.travis/ci.ini new file mode 100644 index 0000000..04b0c20 --- /dev/null +++ b/.travis/ci.ini @@ -0,0 +1,8 @@ +[opcache] +opcache.enable_cli=1 + +[seaslog] +extension = "seaslog.so" + +[swoole] +extension = "swoole.so" \ No newline at end of file diff --git a/.travis/seaslog.install.sh b/.travis/seaslog.install.sh new file mode 100644 index 0000000..0664632 --- /dev/null +++ b/.travis/seaslog.install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +wget https://github.com/SeasX/SeasLog/archive/${SL_VERSION}.tar.gz -O SeasLog.tar.gz +mkdir -p SeasLog +tar -xf SeasLog.tar.gz -C SeasLog --strip-components=1 +rm SeasLog.tar.gz +cd SeasLog +phpize +./configure +make -j$(nproc) +make install \ No newline at end of file diff --git a/.travis/swoole.install.sh b/.travis/swoole.install.sh new file mode 100644 index 0000000..1c546f5 --- /dev/null +++ b/.travis/swoole.install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +wget https://github.com/swoole/swoole-src/archive/v${SW_VERSION}.tar.gz -O swoole.tar.gz +mkdir -p swoole +tar -xf swoole.tar.gz -C swoole --strip-components=1 +rm swoole.tar.gz +cd swoole +phpize +./configure --enable-openssl --enable-mysqlnd +make -j$(nproc) +make install \ No newline at end of file diff --git a/README.md b/README.md index fb1ea1f..a2cea8d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,31 @@ Install the latest version with $ composer require seasx/seas-logger ``` -### Basic Usage +简介 +--------- +* 基于`Swoole`的日志组件,采用`Seaslog`日志模板,符合PSR-3,内置多种Target输出,包括`Kafka`,`SeasStash`,`Websocket`,`Console`。 +* 没有做文件`Target`,在高并发下同时写同一个文件,加锁会影响性能,不加锁的方案还没通过,暂时不支持。(有好的方案请多多指教) + +功能 +------------------------ +* 支持多种输出`Target`,可以自定义输出,继承`AbstractTarget`抽象类 +* 可选记录日志等级,`Target`可以单独过滤已选的日志等级日志 +* `Console`,`Websocket`输出支持彩色字体 +* 支持日志Buffer,定时或定量输出。注意:定时或定量输出由于在输出前日志存在内存,宕机会有丢的风险。 +* 支持`Seaslog`,`LoggerConfig`替换为`SeaslogConfig`,需要最新版`Seaslog`(还没发布),暂时可以用`LoggerConfig` + + +食用方式 +---- +* 日志收集建议使用`docker`目录下的`docker-compose.yaml`启动套件,用`sql`目录下的`seaslog.sql`在`Clickhouse`中创建数据库和表,`LoggerConfig`中添加`KafkaTarget`,即可在`Clickhouse`中看到日志 +* 默认带的BI套件为`Superset`,可以做一些分析 +* 生成环境建议`StyleTarget`或者`WebsocketTarget`仅输出`Warning`和`Error`日志或仅保留`KafkaTarget`,使用`SeaslogConfig`(等待最新版本发布)配置,`KafkaTarget`可以输出所有日志 +* `WebsocketTarget`由于各个框架的`Server`获取方式不一致,需要调用`setGetServer`注册获取`Server`的回调函数返回`\Swoole\Server`,默认会往所有`Websocket`连接发送日志,如果需要过滤`fd`可以自定义`Target`。 +* 支持自定义模板,符号为`%A`,默认在`%M`之前,可以在`registerTemplate`里面设置统一的值,同时会被日志记录方法的`Context`参数中设置`template`键值覆盖。 +* 撸码建议采用`DI`依赖注入的方式注册`Logger` + + +### 非Swoole用法 ```php warning('Hello'); $logger->error('SeasLogger'); ``` -### configuration for laravel/lumen >=5.6 -add seaslog configuration in config/logging.php +### Swoole用法 ```php -'channels' => [ - ... - 'seaslog' => [ - 'driver' => 'custom', - 'via' => \Seasx\SeasLogger\Logger::class, - 'path' => '/path/to/logfile', - ], - ... -] -``` + new StyleTarget([ + 'info',//过滤等级,默认为[]全部输出 + ]), + 'kafka' => new KafkaTarget( + new Producter( + new ProducterConfig([ + 'requiredAck' => 0, + ]), + new Broker([ + 'brokerVersion' => '1.0.0', + ]), new Pool([ + 'uri' => '192.168.5.134:9092' + ])), + [], + 'seaslog_test', + [['task_id', 'string'], ['worker_id', 'string']]//自定义模板添加的处理字段,顺序需要按照日志记录中的template数组一致 + ) + ], [ + 'appName' => 'Seaslog',//应用名:远程发送日志的时候用于区分是哪个应用发送来的 + 'bufferSize' => 1000,//定量:buffer>=时会输出,默认为1 + 'tick' => 3,//定时:每tick秒输出,默认为0,不开启定时 + 'recall_depth' => 2,//与Seaslog配置参数一样,默认为0 + ])); + /** + * 这里可以注册一个回调函数,用来处理RequestID,Request URI,Request Method,Client IP的值 + * 下面是示例代码,具体的设置根据自己需要 + */ + $logger->getConfig()->registerTemplate(function () { + $possibleStyles = (new ConsoleColor())->getPossibleStyles(); + $htmlColors = HtmlColor::getPossibleColors(); + if (($requestVar = Context::get(Logger::CONTEXT_KEY)) === null) { + /** @var Request $serverRequest */ + if (($serverRequest = Context::get('request')) !== null) { + $uri = $serverRequest->getUri(); + $requestId = $serverRequest->getAttribute(AttributeEnum::REQUESTID_ATTRIBUTE); + !$requestId && $requestId = uniqid(); + $requestVar = array_filter([ + '%Q' => $requestId, + '%R' => $uri->getPath(), + '%m' => $serverRequest->getMethod(), + '%I' => ArrayHelper::getValue($serverRequest->getServerParams(), 'remote_addr'), + '%c' => [ + $possibleStyles[rand(0, count($possibleStyles) - 1)], + $htmlColors[rand(0, count($htmlColors) - 1)] + ] + ]); + } else { + $requestVar = array_filter([ + '%Q' => uniqid(), + '%c' => [ + $possibleStyles[rand(0, count($possibleStyles) - 1)], + $htmlColors[rand(0, count($htmlColors) - 1)] + ] + ]); + } + $requestVar['%A'] = ['123', '456'];//%A为自定义字段,会被log里面的template覆盖 + Context::set(Logger::CONTEXT_KEY, $requestVar); + } + return $requestVar; + }); + /* + * 这里区别于标准PSR-3,Context占用两个固定key(module),作用与Seaslog的Logger参数一样,默认值为System + * template为用户自定义模板对应的填充值,默认为[],不填充 + */ + $logger->info("test logger $i", ['module' => 'logger', 'template' => ['abc', 'def']]); +}); -edit .env file to use seaslog -```php -LOG_CHANNEL=seaslog ``` -### See more -[https://github.com/SeasX/SeasLog](https://github.com/SeasX/SeasLog) +配套组件`docker-compose` +------------------------ +* `Clickhouse`日志存储。 +* `ClickhouseWeb`Clickhouse操作Web界面 +* `Zookeeper`Kafka依赖 +* `Kafka`日志队列 +* `Manager`Kafka管理Web界面 +* `Superset`BI分析 +* `mysql`Superset依赖 +* `redis`Superset依赖 +* `Grafana`监控(还没添加) + diff --git a/composer.json b/composer.json index 610068a..e18d8f6 100644 --- a/composer.json +++ b/composer.json @@ -1,41 +1,48 @@ { - "name": "seasx/seas-logger", - "description": "An effective,fast,stable log package for PHP", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "SeasX", - "homepage": "https://github.com/SeasX" + "name": "seasx/seas-logger", + "description": "An effective,fast,stable log package for PHP", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "SeasX", + "homepage": "https://github.com/SeasX" + } + ], + "require": { + "php": "^7.1", + "psr/log": "^1.0.2", + "wujunze/php-cli-color": "^2.4", + "ext-seaslog": "^2.0", + "lcobucci/clock": "^1.0", + "friendsofphp/php-cs-fixer": "^2.11" + }, + "require-dev": { + "swoole/ide-helper": "@dev", + "phpunit/phpunit": "^6.5" + }, + "autoload": { + "psr-4": { + "Seasx\\SeasLogger\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Seasx\\SeasLogger\\Tests\\": "tests/" + } + }, + "scripts": { + "test": [ + "./vendor/bin/phpunit" + ] } - ], - "require": { - "php": "^7.0", - "psr/log": "^1.0.2", - "ext-SeasLog": "^2.0", - "friendsofphp/php-cs-fixer": "^2.11" - }, - "require-dev": { - "phpunit/phpunit": "^6.5" - }, - "autoload": { - "psr-4": { - "Seasx\\SeasLogger\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Seasx\\SeasLogger\\Tests\\": "tests/" - } - }, - "scripts": { - "test": [ - "./vendor/bin/phpunit" - ] - } } + diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..002768c --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,94 @@ +# Use root/example as user/password credentials +version: '3' + +services: + zookeeper: + image: confluentinc/cp-zookeeper:latest + restart: unless-stopped + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-kafka:latest + restart: unless-stopped + ports: + - 9092:9092 + - 5555:5555 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ADVERTISED_HOST_NAME: kafka + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: "5555" + depends_on: + - zookeeper + container_name: kafka + + manager: + image: zenko/kafka-manager + restart: unless-stopped + ports: + - 9091:9000 + environment: + ZK_HOSTS: "zookeeper:2181" + depends_on: + - zookeeper + - kafka + + clickhouse: + image: yandex/clickhouse-server:19.13.3.26 + restart: unless-stopped + ports: + - 8123:8123 + - 9000:9000 + - 9009:9009 + ulimits: + nofile: + soft: 262144 + hard: 262144 + + ckweb: + image: spoonest/clickhouse-tabix-web-client + restart: unless-stopped + environment: + CH_NAME: default + CH_HOST: clickhouse:8123 + depends_on: + - clickhouse + ports: + - 8080:80 + + superset: + image: amancevice/superset + restart: unless-stopped + depends_on: + - redis + - mysql + ports: + - 8088:8088 + volumes: + - ./superset_config.py:/etc/superset/superset_config.py + + mysql: + image: mysql:5 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root + command: --max_connections=512 --explicit_defaults_for_timestamp=1 --sql-mode="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" + --tmp_table_size=18M --thread_cache_size=8 --query_cache_size=0 --myisam_max_sort_file_size=64G + --myisam_sort_buffer_size=35M --key_buffer_size=25M --read_buffer_size=64K --read_rnd_buffer_size=256K --sort_buffer_size=256K + --max_allowed_packet=128M --innodb_flush_log_at_trx_commit=1 --innodb_log_buffer_size=1M + --innodb_buffer_pool_size=47M --innodb_log_file_size=24M --innodb_thread_concurrency=8 --default-time-zone="+8:00" + ports: + - 3306:3306 + + redis: + image: redis:alpine + restart: unless-stopped + ports: + - 6379:6379 + diff --git a/docker/superset_config.py b/docker/superset_config.py new file mode 100644 index 0000000..d6d9871 --- /dev/null +++ b/docker/superset_config.py @@ -0,0 +1,14 @@ +import os + +MAPBOX_API_KEY = os.getenv('MAPBOX_API_KEY', '') +CACHE_CONFIG = { + 'CACHE_TYPE': 'redis', + 'CACHE_DEFAULT_TIMEOUT': 300, + 'CACHE_KEY_PREFIX': 'superset_', + 'CACHE_REDIS_HOST': 'redis', + 'CACHE_REDIS_PORT': 6379, + 'CACHE_REDIS_DB': 1, + 'CACHE_REDIS_URL': 'redis://redis:6379/1'} +SQLALCHEMY_DATABASE_URI = 'mysql://root:root@mysql:3306/superset' +SQLALCHEMY_TRACK_MODIFICATIONS = True +SECRET_KEY = 'thisISaSECRET_1234' \ No newline at end of file diff --git a/example/logger.php b/example/logger.php new file mode 100644 index 0000000..58bb7f4 --- /dev/null +++ b/example/logger.php @@ -0,0 +1,72 @@ + new StyleTarget([ + 'info',//过滤等级,默认为[]全部输出 + ]), + 'kafka' => new KafkaTarget( + new Producter( + new ProducterConfig([ + 'requiredAck' => 0, + ]), + new Broker([ + 'brokerVersion' => '1.0.0', + ]), new Pool([ + 'uri' => '192.168.5.134:9092' + ])), + [], + 'seaslog_test', + [['task_id', 'string'], ['worker_id', 'string']]//自定义模板添加的处理字段,顺序需要按照日志记录中的template数组一致 + ) + ], [ + 'appName' => 'Seaslog',//应用名:远程发送日志的时候用于区分是哪个应用发送来的 + 'bufferSize' => 1,//定量:buffer>=时会输出,默认为1,每次记录都会输出 + 'tick' => 0,//定时:每tick秒输出,默认为0,不开启定时 + 'recall_depth' => 2,//与Seaslog配置参数一样,默认为0 + ])); + /** + * 这里可以注册两个回调函数,会在log方法前执行,可以用来处理RequestID,Request URI,Request Method,Client IP的值 + * 下面是示例代码,具体的设置根据自己需要 + */ + $logger->getConfig()->registerTemplate(function () { + $possibleStyles = (new Colors())->getForegroundColors(); + $htmlColors = HtmlColor::getPossibleColors(); + if (($requestVar = Context::get(Logger::CONTEXT_KEY)) === null) { + $requestVar = array_filter([ + '%Q' => uniqid(), + '%c' => [ + 'console' => $possibleStyles[rand(0, count($possibleStyles) - 1)], + 'websocket' => $htmlColors[rand(0, count($htmlColors) - 1)] + ] + ]); + $requestVar['%A'] = ['123', '456']; + Context::set(Logger::CONTEXT_KEY, $requestVar); + } + return $requestVar; + }); + /* + * 这里区别于标准PSR-3,Context占用两个固定key(module),作用与Seaslog的Logger参数一样,默认值为System + * template为用户自定义模板对应的填充值,默认为[],不填充 + */ + for ($i = 0; $i < 1; $i++) { + $logger->info("test logger $i", ['module' => 'logger', 'template' => ['abc', 'def']]); + } +}); + +swoole_event_wait(); \ No newline at end of file diff --git a/sql/seaslog.sql b/sql/seaslog.sql new file mode 100644 index 0000000..ef91b8b --- /dev/null +++ b/sql/seaslog.sql @@ -0,0 +1,52 @@ +create database if not exists logs; + +CREATE TABLE if not exists logs.kafka_seaslog +( + `appname` String, + `datetime` DateTime, + `level` String, + `request_uri` String, + `request_method` String, + `clientip` String, + `requestid` String, + `filename` String, + `memoryusage` UInt64, + `message` String +) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka:29092', + kafka_topic_list = 'seaslog', + kafka_group_name = 'clickhouse', + kafka_format = 'JSONEachRow', + kafka_skip_broken_messages = 1, + kafka_num_consumers = 1; +--消费者数量根据情况自定义 + +CREATE TABLE if not exists logs.seaslog +( + `appname` String, + `datetime` DateTime, + `level` String, + `request_uri` String, + `request_method` String, + `clientip` String, + `requestid` String, + `filename` String, + `memoryusage` UInt64, + `message` String +) ENGINE = MergeTree PARTITION BY toYYYYMM(datetime) + ORDER BY + (datetime, + appname) SETTINGS index_granularity = 8192; + +CREATE MATERIALIZED VIEW if not exists logs.consumer_seaslog TO logs.seaslog AS +SELECT appname, + datetime, + level, + request_uri, + request_method, + clientip, + requestid, + filename, + memoryusage, + message +FROM logs.kafka_seaslog; + diff --git a/src/AbstractConfig.php b/src/AbstractConfig.php new file mode 100644 index 0000000..9147eee --- /dev/null +++ b/src/AbstractConfig.php @@ -0,0 +1,127 @@ + $value) { + if (property_exists($this, $name) && $name !== 'targetList') { + $this->$name = $value; + } + } + $this->targetList = $target; + foreach ($this->targetList as $target) { + $target->setTemplate($this->template)->setCustomerFieldType($this->customerType)->setSplit($this->split); + } + register_shutdown_function(function () { + $this->flush(true); + }); + $this->tick > 0 && Timer::tick($this->tick * 1000, [$this, 'flush'], [true]); + } + + /** + * @return array + */ + public function getTemplate(): array + { + return $this->template; + } + + /** + * @param bool $flush + */ + abstract public function flush(bool $flush = false): void; + + /** + * @return array + */ + abstract public function getBuffer(): array; + + /** + * @return string + */ + abstract public function getDatetimeFormat(): string; + + /** + * @param string $format + * @return bool + */ + abstract public function setDatetimeFormat(string $format): bool; + /** + * @return array + */ + public static function getSupportFieldType(): array + { + return static::$supportField; + } + + /** + * @param callable $userTemplate + */ + public function registerTemplate(callable $userTemplate): void + { + $this->userTemplate = $userTemplate; + } + + /** + * @param string $level + * @param string $message + * @param array $context + */ + abstract public function log(string $level, string $message, array $context = []): void; + + /** + * @return array + */ + protected function getUserTemplate(): array + { + if ($this->userTemplate) { + $template = call_user_func($this->userTemplate); + $template = $template ?? []; + } else { + $template = []; + } + return $template; + } +} \ No newline at end of file diff --git a/src/ArrayHelper.php b/src/ArrayHelper.php new file mode 100644 index 0000000..5c07d84 --- /dev/null +++ b/src/ArrayHelper.php @@ -0,0 +1,71 @@ +$key; + } elseif (is_array($array)) { + return (isset($array[$key]) || array_key_exists($key, $array)) ? $array[$key] : $default; + } + + return $default; + } + + /** + * @param $array + * @param $key + * @param mixed|null $default + * @return mixed|null + */ + public static function remove(&$array, $key, $default = null) + { + if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { + $value = $array[$key]; + unset($array[$key]); + + return $value; + } + + return $default; + } +} diff --git a/src/Context.php b/src/Context.php new file mode 100644 index 0000000..6b113ac --- /dev/null +++ b/src/Context.php @@ -0,0 +1,57 @@ + '#F0F8FF', + 'AntiqueWhite' => '#FAEBD7', + 'Aqua' => '#00FFFF', + 'Aquamarine' => '#7FFFD4', + 'Azure ' => '#F0FFFF', + 'Beige' => '#F5F5DC', + 'Bisque' => '#FFE4C4', + 'Black' => '#000000', + 'BlanchedAlmond' => '#FFEBCD', + 'Blue' => '#0000FF', + 'BlueViolet' => '#8A2BE2', + 'Brown' => '#A52A2A', + 'BurlyWood' => '#DEB887', + 'CadetBlue' => '#5F9EA0', + 'Chartreuse' => '#7FFF00', + 'Chocolate' => '#D2691E', + 'Coral' => '#FF7F50', + 'CornflowerBlue' => '#6495ED', + 'Cornsilk' => '#FFF8DC', + 'Crimson' => '#DC143C', + 'Cyan' => '#00FFFF', + 'DarkBlue' => '#00008B', + 'DarkCyan' => '#008B8B', + 'DarkGoldenRod' => '#B8860B', + 'DarkGray' => '#A9A9A9', + 'DarkGreen' => '#006400', + 'DarkKhaki' => '#BDB76B', + 'DarkMagenta' => '#8B008B', + 'DarkOliveGreen' => '#556B2F', + 'Darkorange' => '#FF8C00', + 'DarkOrchid' => '#9932CC', + 'DarkRed' => '#8B0000', + 'DarkSalmon' => '#E9967A', + 'DarkSeaGreen' => '#8FBC8F', + 'DarkSlateBlue' => '#483D8B', + 'DarkSlateGray' => '#2F4F4F', + 'DarkTurquoise' => '#00CED1', + 'DarkViolet' => '#9400D3', + 'DeepPink' => '#FF1493', + 'DeepSkyBlue' => '#00BFFF', + 'DimGray' => '#696969', + 'DodgerBlue' => '#1E90FF', + 'Feldspar' => '#D19275', + 'FireBrick' => '#B22222', + 'FloralWhite' => '#FFFAF0', + 'ForestGreen' => '#228B22', + 'Fuchsia' => '#FF00FF', + 'Gainsboro' => '#DCDCDC', + 'GhostWhite' => '#F8F8FF', + 'Gold' => '#FFD700', + 'GoldenRod' => '#DAA520', + 'Gray' => '#808080', + 'Green' => '#008000', + 'GreenYellow' => '#ADFF2F', + 'HoneyDew' => '#F0FFF0', + 'HotPink' => '#FF69B4', + 'IndianRed' => '#CD5C5C', + 'Indigo' => '#4B0082', + 'Ivory' => '#FFFFF0', + 'Khaki' => '#F0E68C', + 'Lavender' => '#E6E6FA', + 'LavenderBlush' => '#FFF0F5', + 'LawnGreen' => '#7CFC00', + 'LemonChiffon' => '#FFFACD', + 'LightBlue' => '#ADD8E6', + 'LightCoral' => '#F08080', + 'LightCyan' => '#E0FFFF', + 'LightGray' => '#D3D3D3', + 'LightGreen' => '#90EE90', + 'LightPink' => '#FFB6C1', + 'LightSalmon' => '#FFA07A', + 'LightSeaGreen' => '#20B2AA', + 'LightSkyBlue' => '#87CEFA', + 'LightSlateGray' => '#778899', + 'LightSteelBlue' => '#B0C4DE', + 'LightYellow' => '#FFFFE0', + 'Lime' => '#00FF00', + 'LimeGreen' => '#32CD32', + 'Magenta' => '#FF00FF', + 'Maroon' => '#800000', + 'MediumAquaMarine' => '#66CDAA', + 'MediumBlue' => '#0000CD', + 'MediumOrchid' => '#BA55D3', + 'MediumPurple' => '#9370DB', + 'MediumSeaGreen' => '#3CB371', + 'MediumSlateBlue' => '#7B68EE', + 'MediumSpringGreen' => '#00FA9A', + 'MediumTurquoise' => '#48D1CC', + 'MediumVioletRed' => '#C71585', + 'MidnightBlue' => '#191970', + 'MintCream' => '#F5FFFA', + 'MistyRose' => '#FFE4E1', + 'Moccasin' => '#FFE4B5', + 'NavajoWhite' => '#FFDEAD', + 'Navy' => '#000080', + 'OldLace' => '#FDF5E6', + 'Olive' => '#808000', + 'OliveDrab' => '#6B8E23', + 'Orange' => '#FFA500', + 'OrangeRed' => '#FF4500', + 'Orchid' => '#DA70D6', + 'PaleGoldenRod' => '#EEE8AA', + 'PaleGreen' => '#98FB98', + 'PaleTurquoise' => '#AFEEEE', + 'PaleVioletRed' => '#DB7093', + 'PapayaWhip' => '#FFEFD5', + 'PeachPuff' => '#FFDAB9', + 'Peru' => '#CD853F', + 'Pink' => '#FFC0CB', + 'Plum' => '#DDA0DD', + 'PowderBlue' => '#B0E0E6', + 'Purple' => '#800080', + 'Red' => '#FF0000', + 'RosyBrown' => '#BC8F8F', + 'RoyalBlue' => '#4169E1', + 'SaddleBrown' => '#8B4513', + 'Salmon' => '#FA8072', + 'SandyBrown' => '#F4A460', + 'SeaGreen' => '#2E8B57', + 'SeaShell' => '#FFF5EE', + 'Sienna' => '#A0522D', + 'Silver' => '#C0C0C0', + 'SkyBlue' => '#87CEEB', + 'SlateBlue' => '#6A5ACD', + 'SlateGray' => '#708090', + 'Snow' => '#FFFAFA', + 'SpringGreen' => '#00FF7F', + 'SteelBlue' => '#4682B4', + 'Tan' => '#D2B48C', + 'Teal' => '#008080', + 'Thistle' => '#D8BFD8', + 'Tomato' => '#FF6347', + 'Turquoise' => '#40E0D0', + 'Violet' => '#EE82EE', + 'Wheat' => '#F5DEB3', + 'White' => '#FFFFFF', + 'WhiteSmoke' => '#F5F5F5', + 'Yellow' => '#FFFF00', + 'YellowGreen' => '#9ACD32' + ); + + /** + * @param string $color + * @return string + */ + public static function getColor(string $color): string + { + return (string)ArrayHelper::getValue(static::$colors, $color, ''); + } + + /** + * @return array + */ + public static function getPossibleColors(): array + { + return array_keys(static::$colors); + } + + /** + * @return array + */ + public static function getPossibleColorsRGB(): array + { + return array_values(static::$colors); + } +} \ No newline at end of file diff --git a/src/Kafka/Broker.php b/src/Kafka/Broker.php new file mode 100644 index 0000000..0c4040d --- /dev/null +++ b/src/Kafka/Broker.php @@ -0,0 +1,115 @@ + $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } + } + } + + /** + * @return int + */ + public function getGroupBrokerId(): int + { + return $this->groupBrokerId; + } + + /** + * @param int $brokerId + */ + public function setGroupBrokerId(int $brokerId): void + { + $this->groupBrokerId = $brokerId; + } + + /** + * @param array $topics + * @param array $brokersResult + * @return bool + */ + public function setData(array $topics, array $brokersResult): bool + { + $brokers = []; + + foreach ($brokersResult as $value) { + $brokers[$value['nodeId']] = $value['host'] . ':' . $value['port']; + } + + $changed = false; + + if (serialize($this->brokers) !== serialize($brokers)) { + $this->brokers = $brokers; + + $changed = true; + } + + $newTopics = []; + foreach ($topics as $topic) { + if ((int)$topic['errorCode'] !== Protocol::NO_ERROR) { + continue; + } + + $item = []; + + foreach ($topic['partitions'] as $part) { + $item[$part['partitionId']] = $part['leader']; + } + + $newTopics[$topic['topicName']] = $item; + } + + if (serialize($this->topics) !== serialize($newTopics)) { + $this->topics = $newTopics; + + $changed = true; + } + + return $changed; + } + + /** + * @return array + */ + public function getTopics(): array + { + return $this->topics; + } + + /** + * @return string[] + */ + public function getBrokers(): array + { + return $this->brokers; + } + + public function clear(): void + { + $this->brokers = []; + } +} \ No newline at end of file diff --git a/src/Kafka/Config.php b/src/Kafka/Config.php new file mode 100644 index 0000000..8c1787e --- /dev/null +++ b/src/Kafka/Config.php @@ -0,0 +1,210 @@ + $value) { + $method = 'set' . ucfirst($name); + $this->$method($value); + } + } + + /** + * @param string $name + * @param mixed[] $args + * + * @return bool|mixed + */ + public function __call(string $name, array $args) + { + $isGetter = strpos($name, 'get') === 0 || strpos($name, 'iet') === 0; + $isSetter = strpos($name, 'set') === 0; + + if (!$isGetter && !$isSetter) { + return false; + } + + $option = lcfirst(substr($name, 3)); + + if ($isGetter) { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + + if (isset(static::$defaults[$option])) { + return static::$defaults[$option]; + } + + return false; + } + + if (count($args) !== 1) { + return false; + } + + $this->options[$option] = array_shift($args); + + // check todo + return true; + } + + /** + * @param string $val + * @throws Exception + */ + public function setClientId(string $val): void + { + $client = trim($val); + + if ($client === '') { + throw new Exception('Set clientId value is invalid, must is not empty string.'); + } + + $this->options['clientId'] = $client; + } + + /** + * @param string $version + * @throws Exception + */ + public function setBrokerVersion(string $version): void + { + $version = trim($version); + + if ($version === '' || version_compare($version, '0.8.0', '<')) { + throw new Exception('Set broker version value is invalid, must is not empty string and gt 0.8.0.'); + } + + $this->options['brokerVersion'] = $version; + } + + /** + * @param string $brokerList + * @throws Exception + */ + public function setMetadataBrokerList(string $brokerList): void + { + $brokerList = trim($brokerList); + + $brokers = array_filter( + explode(',', $brokerList), + function (string $broker): bool { + return preg_match('/^(.*:[\d]+)$/', $broker) === 1; + } + ); + + if (empty($brokers)) { + throw new Exception( + 'Broker list must be a comma-separated list of brokers (format: "host:port"), with at least one broker' + ); + } + + $this->options['metadataBrokerList'] = $brokers; + } + + public function clear(): void + { + $this->options = []; + } + + /** + * @param int $messageMaxBytes + * @throws Exception + */ + public function setMessageMaxBytes(int $messageMaxBytes): void + { + if ($messageMaxBytes < 1000 || $messageMaxBytes > 1000000000) { + throw new Exception('Set message max bytes value is invalid, must set it 1000 .. 1000000000'); + } + $this->options['messageMaxBytes'] = $messageMaxBytes; + } + + /** + * @param int $metadataRequestTimeoutMs + * @throws Exception + */ + public function setMetadataRequestTimeoutMs(int $metadataRequestTimeoutMs): void + { + if ($metadataRequestTimeoutMs < 10 || $metadataRequestTimeoutMs > 900000) { + throw new Exception('Set metadata request timeout value is invalid, must set it 10 .. 900000'); + } + $this->options['metadataRequestTimeoutMs'] = $metadataRequestTimeoutMs; + } + + /** + * @param int $metadataRefreshIntervalMs + * @throws Exception + */ + public function setMetadataRefreshIntervalMs(int $metadataRefreshIntervalMs): void + { + if ($metadataRefreshIntervalMs < 10 || $metadataRefreshIntervalMs > 3600000) { + throw new Exception('Set metadata refresh interval value is invalid, must set it 10 .. 3600000'); + } + $this->options['metadataRefreshIntervalMs'] = $metadataRefreshIntervalMs; + } + + /** + * @param int $metadataMaxAgeMs + * @throws Exception + */ + public function setMetadataMaxAgeMs(int $metadataMaxAgeMs): void + { + if ($metadataMaxAgeMs < 1 || $metadataMaxAgeMs > 86400000) { + throw new Exception('Set metadata max age value is invalid, must set it 1 .. 86400000'); + } + $this->options['metadataMaxAgeMs'] = $metadataMaxAgeMs; + } +} diff --git a/src/Kafka/InvalidRecordInSet.php b/src/Kafka/InvalidRecordInSet.php new file mode 100644 index 0000000..ccd08f4 --- /dev/null +++ b/src/Kafka/InvalidRecordInSet.php @@ -0,0 +1,52 @@ +requestHeader('seaslog-kafka', self::METADATA_REQUEST, self::METADATA_REQUEST); + $data = self::encodeArray($payloads, [$this, 'encodeString'], self::PACK_INT16); + $data = self::encodeString($header . $data, self::PACK_INT32); + + return $data; + } + + /** + * @param string $data + * @return array + * @throws Exception + */ + public function decode(string $data): array + { + $offset = 0; + $brokerRet = $this->decodeArray(substr($data, $offset), [$this, 'metaBroker']); + $offset += $brokerRet['length']; + $topicMetaRet = $this->decodeArray(substr($data, $offset), [$this, 'metaTopicMetaData']); + $offset += $topicMetaRet['length']; + + $result = [ + 'brokers' => $brokerRet['data'], + 'topics' => $topicMetaRet['data'], + ]; + + return $result; + } + + /** + * @param string $data + * @return array + * @throws Exception + */ + protected function metaBroker(string $data): array + { + $offset = 0; + $nodeId = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + $hostNameInfo = $this->decodeString(substr($data, $offset), self::BIT_B16); + $offset += $hostNameInfo['length']; + $port = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + + return [ + 'length' => $offset, + 'data' => [ + 'host' => $hostNameInfo['data'], + 'port' => $port, + 'nodeId' => $nodeId, + ], + ]; + } + + /** + * @param string $data + * @return array + * @throws Exception + */ + protected function metaTopicMetaData(string $data): array + { + $offset = 0; + $topicErrCode = self::unpack(self::BIT_B16_SIGNED, substr($data, $offset, 2)); + $offset += 2; + $topicInfo = $this->decodeString(substr($data, $offset), self::BIT_B16); + $offset += $topicInfo['length']; + $partitionsMeta = $this->decodeArray(substr($data, $offset), [$this, 'metaPartitionMetaData']); + $offset += $partitionsMeta['length']; + + return [ + 'length' => $offset, + 'data' => [ + 'topicName' => $topicInfo['data'], + 'errorCode' => $topicErrCode, + 'partitions' => $partitionsMeta['data'], + ], + ]; + } + + /** + * @param string $data + * @return array + * @throws Exception + */ + protected function metaPartitionMetaData(string $data): array + { + $offset = 0; + $errcode = self::unpack(self::BIT_B16_SIGNED, substr($data, $offset, 2)); + $offset += 2; + $partId = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + $leader = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + $replicas = $this->decodePrimitiveArray(substr($data, $offset), self::BIT_B32); + $offset += $replicas['length']; + $isr = $this->decodePrimitiveArray(substr($data, $offset), self::BIT_B32); + $offset += $isr['length']; + + return [ + 'length' => $offset, + 'data' => [ + 'partitionId' => $partId, + 'errorCode' => $errcode, + 'replicas' => $replicas['data'], + 'leader' => $leader, + 'isr' => $isr['data'], + ], + ]; + } +} diff --git a/src/Kafka/Produce.php b/src/Kafka/Produce.php new file mode 100644 index 0000000..a4e32b9 --- /dev/null +++ b/src/Kafka/Produce.php @@ -0,0 +1,304 @@ +clock = $clock ?: new SystemClock(); + } + + /** + * @param array $payloads + * @return string + */ + public function encode(array $payloads = []): string + { + if (!isset($payloads['data'])) { + throw new InvalidArgumentException('given procude data invalid. `data` is undefined.'); + } + + $header = $this->requestHeader('seaslog-kafka', 0, self::PRODUCE_REQUEST); + $data = self::pack(self::BIT_B16, (string)($payloads['required_ack'] ?? 0)); + $data .= self::pack(self::BIT_B32, (string)($payloads['timeout'] ?? 100)); + $data .= self::encodeArray( + $payloads['data'], + [$this, 'encodeProduceTopic'], + $payloads['compression'] ?? self::COMPRESSION_NONE + ); + + return self::encodeString($header . $data, self::PACK_INT32); + } + + /** + * @param string $data + * @return array + * @throws \Exception + */ + public function decode(string $data): array + { + $offset = 0; + $version = $this->getApiVersion(self::PRODUCE_REQUEST); + $ret = $this->decodeArray(substr($data, $offset), [$this, 'produceTopicPair'], $version); + $offset += $ret['length']; + $throttleTime = 0; + + if ($version === self::API_VERSION2) { + $throttleTime = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + } + + return ['throttleTime' => $throttleTime, 'data' => $ret['data']]; + } + + /** + * encode signal part + * + * @param mixed[] $values + * + * @param int $compression + * @return string + */ + protected function encodeProducePartition(array $values, int $compression): string + { + if (!isset($values['partition_id'])) { + throw new InvalidArgumentException('given produce data invalid. `partition_id` is undefined.'); + } + + if (!isset($values['messages']) || empty($values['messages'])) { + throw new InvalidArgumentException('given produce data invalid. `messages` is undefined.'); + } + + $data = self::pack(self::BIT_B32, (string)$values['partition_id']); + $data .= self::encodeString( + $this->encodeMessageSet((array)$values['messages'], $compression), + self::PACK_INT32 + ); + + return $data; + } + + /** + * encode message set + * N.B., MessageSets are not preceded by an int32 like other array elements + * in the protocol. + * + * @param string[]|string[][] $messages + * + * @param int $compression + * @return string + */ + protected function encodeMessageSet(array $messages, int $compression = self::COMPRESSION_NONE): string + { + $data = ''; + $next = 0; + + foreach ($messages as $message) { + $encodedMessage = $this->encodeMessage($message); + + $data .= self::pack(self::BIT_B64, (string)$next) + . self::encodeString($encodedMessage, self::PACK_INT32); + + ++$next; + } + + if ($compression === self::COMPRESSION_NONE) { + return $data; + } + + return self::pack(self::BIT_B64, '0') + . self::encodeString($this->encodeMessage($data, $compression), self::PACK_INT32); + } + + /** + * @param string[]|string $message + * + * @param int $compression + * @return string + */ + protected function encodeMessage($message, int $compression = self::COMPRESSION_NONE): string + { + $magic = $this->computeMagicByte(); + $attributes = $this->computeAttributes($magic, $compression, $this->computeTimestampType($magic)); + + $data = self::pack(self::BIT_B8, (string)$magic); + $data .= self::pack(self::BIT_B8, (string)$attributes); + + if ($magic >= self::MESSAGE_MAGIC_VERSION1) { + $data .= self::pack(self::BIT_B64, $this->clock->now()->format('Uv')); + } + + $key = ''; + + if (is_array($message)) { + $key = $message['key']; + $message = $message['value']; + } + + // message key + $data .= self::encodeString($key, self::PACK_INT32); + + // message value + $data .= self::encodeString($message, self::PACK_INT32, $compression); + + $crc = (string)crc32($data); + + // int32 -- crc code string data + $message = self::pack(self::BIT_B32, $crc) . $data; + + return $message; + } + + private function computeMagicByte(): int + { + if ($this->getApiVersion(self::PRODUCE_REQUEST) === self::API_VERSION2) { + return self::MESSAGE_MAGIC_VERSION1; + } + + return self::MESSAGE_MAGIC_VERSION0; + } + + private function computeAttributes(int $magic, int $compression, int $timestampType): int + { + $attributes = 0; + + if ($compression !== self::COMPRESSION_NONE) { + $attributes |= self::COMPRESSION_CODEC_MASK & $compression; + } + + if ($magic === self::MESSAGE_MAGIC_VERSION0) { + return $attributes; + } + + if ($timestampType === self::TIMESTAMP_LOG_APPEND_TIME) { + $attributes |= self::TIMESTAMP_TYPE_MASK; + } + + return $attributes; + } + + public function computeTimestampType(int $magic): int + { + if ($magic === self::MESSAGE_MAGIC_VERSION0) { + return self::TIMESTAMP_NONE; + } + + return self::TIMESTAMP_CREATE_TIME; + } + + /** + * encode signal topic + * + * @param mixed[] $values + * + * @param int $compression + * @return string + */ + protected function encodeProduceTopic(array $values, int $compression): string + { + if (!isset($values['topic_name'])) { + throw new InvalidArgumentException('given produce data invalid. `topic_name` is undefined.'); + } + + if (!isset($values['partitions']) || empty($values['partitions'])) { + throw new InvalidArgumentException('given produce data invalid. `partitions` is undefined.'); + } + + $topic = self::encodeString($values['topic_name'], self::PACK_INT16); + $partitions = self::encodeArray($values['partitions'], [$this, 'encodeProducePartition'], $compression); + + return $topic . $partitions; + } + + /** + * decode produce topic pair response + * + * @param string $data + * @param int $version + * @return mixed[] + * + * @throws \Exception + */ + protected function produceTopicPair(string $data, int $version): array + { + $offset = 0; + $topicInfo = $this->decodeString($data, self::BIT_B16); + $offset += $topicInfo['length']; + $ret = $this->decodeArray(substr($data, $offset), [$this, 'producePartitionPair'], $version); + $offset += $ret['length']; + + return [ + 'length' => $offset, + 'data' => [ + 'topicName' => $topicInfo['data'], + 'partitions' => $ret['data'], + ], + ]; + } + + /** + * decode produce partition pair response + * + * @param string $data + * @param int $version + * @return mixed[] + * + * @throws \Exception + */ + protected function producePartitionPair(string $data, int $version): array + { + $offset = 0; + $partitionId = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + $errorCode = self::unpack(self::BIT_B16_SIGNED, substr($data, $offset, 2)); + $offset += 2; + self::unpack(self::BIT_B64, substr($data, $offset, 8)); + $offset += 8; + $timestamp = 0; + + if ($version === self::API_VERSION2) { + $timestamp = self::unpack(self::BIT_B64, substr($data, $offset, 8)); + $offset += 8; + } + + return [ + 'length' => $offset, + 'data' => [ + 'partition' => $partitionId, + 'errorCode' => $errorCode, + 'offset' => $offset, + 'timestamp' => $timestamp, + ], + ]; + } +} diff --git a/src/Kafka/Producter.php b/src/Kafka/Producter.php new file mode 100644 index 0000000..20f0d16 --- /dev/null +++ b/src/Kafka/Producter.php @@ -0,0 +1,166 @@ +config = $config; + $this->broker = $broker; + $this->pool = $pool; + $this->recordValidator = new RecordValidator(); + ProtocolTool::init($config->getBrokerVersion()); + } + + /** + * @param array $recordSet + * @param callable|null $callback + * @throws Exception + */ + public function send(array $recordSet, ?callable $callback = null): void + { + static $isInit = false; + if (!$isInit) { + $isInit = true; + $this->syncMeta(); + } + $requiredAck = $this->config->getRequiredAck(); + $timeout = $this->config->getTimeout(); + $compression = $this->config->getCompression(); + if (empty($recordSet)) { + return; + } + + $recordSet = array_merge($recordSet, array_splice($this->msgBuffer, 0)); + $sendData = $this->convertRecordSet($recordSet); + foreach ($sendData as $brokerId => $topicList) { + $connect = $this->pool->getConnection(); + $params = [ + 'required_ack' => $requiredAck, + 'timeout' => $timeout, + 'data' => $topicList, + 'compression' => $compression, + ]; + + $requestData = ProtocolTool::encode(ProtocolTool::PRODUCE_REQUEST, $params); + rgo(function () use ($connect, $requestData, $requiredAck, $callback) { + if ($requiredAck !== 0) { + $connect->send($requestData); + $dataLen = Protocol::unpack(Protocol::BIT_B32, $connect->recv(4)); + $recordSet = $connect->recv((int)$dataLen); + $this->pool->release($connect); + Protocol::unpack(Protocol::BIT_B32, substr($recordSet, 0, 4)); + $callback && $callback(ProtocolTool::decode(ProtocolTool::PRODUCE_REQUEST, + substr($recordSet, 4))); + } else { + $connect->send($requestData); + $this->pool->release($connect); + } + }); + } + } + + /** + * @throws Exception + */ + public function syncMeta(): void + { + $socket = $this->pool->getConnection(); + rgo(function () use ($socket) { + while (true) { + try { + $params = []; + $requestData = ProtocolTool::encode(ProtocolTool::METADATA_REQUEST, $params); + $socket->send($requestData); + $dataLen = Protocol::unpack(Protocol::BIT_B32, $socket->recv(4)); + $data = $socket->recv((int)$dataLen); + Protocol::unpack(Protocol::BIT_B32, substr($data, 0, 4)); + $result = ProtocolTool::decode(ProtocolTool::METADATA_REQUEST, substr($data, 4)); + if (!isset($result['brokers'], $result['topics'])) { + throw new Exception('Get metadata is fail, brokers or topics is null.'); + } + $this->broker->setData($result['topics'], $result['brokers']); + } finally { + Co::sleep(30); + } + } + }); + } + + /** + * @param string[][] $recordSet + * + * @return mixed[] + * @throws InvalidRecordInSet + */ + protected function convertRecordSet(array $recordSet): array + { + $sendData = []; + while (empty($topics = $this->broker->getTopics())) { + Co::sleep(0.5); + } + + foreach ($recordSet as $record) { + + $this->recordValidator->validate($record, $topics); + + $topicMeta = $topics[$record['topic']]; + $partNums = array_keys($topicMeta); + shuffle($partNums); + + $partId = isset($record['partId'], $topicMeta[$record['partId']]) ? $record['partId'] : $partNums[0]; + + $brokerId = $topicMeta[$partId]; + $topicData = []; + if (isset($sendData[$brokerId][$record['topic']])) { + $topicData = $sendData[$brokerId][$record['topic']]; + } + + $partition = []; + if (isset($topicData['partitions'][$partId])) { + $partition = $topicData['partitions'][$partId]; + } + + $partition['partition_id'] = $partId; + + if (trim($record['key'] ?? '') !== '') { + $partition['messages'][] = ['value' => $record['value'], 'key' => $record['key']]; + } else { + $partition['messages'][] = $record['value']; + } + + $topicData['partitions'][$partId] = $partition; + $topicData['topic_name'] = $record['topic']; + $sendData[$brokerId][$record['topic']] = $topicData; + } + + return $sendData; + } +} diff --git a/src/Kafka/ProducterConfig.php b/src/Kafka/ProducterConfig.php new file mode 100644 index 0000000..adeb0ca --- /dev/null +++ b/src/Kafka/ProducterConfig.php @@ -0,0 +1,94 @@ + 'seaslog-kafka', + 'brokerVersion' => '0.10.1.0', + 'metadataBrokerList' => '', + 'messageMaxBytes' => 1000000, + 'metadataRequestTimeoutMs' => 60000, + 'metadataRefreshIntervalMs' => 300000, + 'metadataMaxAgeMs' => -1, + 'requiredAck' => 1, + 'timeout' => 5000, + 'requestTimeout' => 6000, + 'compression' => Protocol::COMPRESSION_NONE, + ]; + + /** + * @param int $requestTimeout + * @throws Exception + */ + public function setRequestTimeout(int $requestTimeout): void + { + if ($requestTimeout < 1 || $requestTimeout > 900000) { + throw new NotSupportedException('Set request timeout value is invalid, must set it 1 .. 900000'); + } + + $this->options['requestTimeout'] = $requestTimeout; + } + + /** + * @param int $timeout + * @throws Exception + */ + public function setTimeout(int $timeout): void + { + if ($timeout < 1 || $timeout > 900000) { + throw new NotSupportedException('Set timeout value is invalid, must set it 1 .. 900000'); + } + + $this->options['timeout'] = $timeout; + } + + /** + * @param int $requiredAck + * @throws Exception + */ + public function setRequiredAck(int $requiredAck): void + { + if ($requiredAck < -1 || $requiredAck > 1000) { + throw new NotSupportedException('Set required ack value is invalid, must set it -1 .. 1000'); + } + + $this->options['requiredAck'] = $requiredAck; + } + + /** + * @param int $compression + * @throws Exception + */ + public function setCompression(int $compression): void + { + if (!in_array($compression, self::COMPRESSION_OPTIONS, true)) { + throw new NotSupportedException('Compression must be one the Produce::COMPRESSION_* constants'); + } + + $this->options['compression'] = $compression; + } +} diff --git a/src/Kafka/Protocol.php b/src/Kafka/Protocol.php new file mode 100644 index 0000000..168e82d --- /dev/null +++ b/src/Kafka/Protocol.php @@ -0,0 +1,496 @@ +version = $version; + } + + /** + * @param array $array + * @param callable $func + * @param int|null $options + * @return string + */ + public static function encodeArray(array $array, callable $func, ?int $options = null): string + { + $arrayCount = count($array); + + $body = ''; + foreach ($array as $value) { + $body .= $options !== null ? $func($value, $options) : $func($value); + } + + return self::pack(self::BIT_B32, (string)$arrayCount) . $body; + } + + public static function pack(string $type, string $data): string + { + if ($type !== self::BIT_B64) { + return pack($type, $data); + } + + if ((int)$data === -1) { // -1L + return hex2bin('ffffffffffffffff'); + } + + if ((int)$data === -2) { // -2L + return hex2bin('fffffffffffffffe'); + } + + $left = 0xffffffff00000000; + $right = 0x00000000ffffffff; + + $l = ($data & $left) >> 32; + $r = $data & $right; + + return pack($type, $l, $r); + } + + /** + * Get kafka api text + * @param int $apikey + * @return string + */ + public static function getApiText(int $apikey): string + { + $apis = [ + self::PRODUCE_REQUEST => 'ProduceRequest', + self::METADATA_REQUEST => 'MetadataRequest' + ]; + + return $apis[$apikey] ?? 'Unknown message'; + } + + /** + * @param string $clientId + * @param int $correlationId + * @param int $apiKey + * @return string + */ + public function requestHeader(string $clientId, int $correlationId, int $apiKey): string + { + // int16 -- apiKey int16 -- apiVersion int32 correlationId + $binData = self::pack(self::BIT_B16, (string)$apiKey); + $binData .= self::pack(self::BIT_B16, (string)$this->getApiVersion($apiKey)); + $binData .= self::pack(self::BIT_B32, (string)$correlationId); + + // concat client id + $binData .= self::encodeString($clientId, self::PACK_INT16); + + return $binData; + } + + /** + * Get kafka api version according to specify kafka broker version + * @param int $apikey + * @return int + */ + public function getApiVersion(int $apikey): int + { + switch ($apikey) { + case self::METADATA_REQUEST: + return self::API_VERSION0; + case self::PRODUCE_REQUEST: + if (version_compare($this->version, '0.10.0') >= 0) { + return self::API_VERSION2; + } + + if (version_compare($this->version, '0.9.0') >= 0) { + return self::API_VERSION1; + } + + return self::API_VERSION0; + } + + // default + return self::API_VERSION0; + } + + /** + * @param string $string + * @param int $bytes + * @param int $compression + * @return string + */ + public static function encodeString(string $string, int $bytes, int $compression = self::COMPRESSION_NONE): string + { + $packLen = $bytes === self::PACK_INT32 ? self::BIT_B32 : self::BIT_B16; + $string = self::compress($string, $compression); + + return self::pack($packLen, (string)strlen($string)) . $string; + } + + /** + * @param string $string + * @param int $compression + * @return string + */ + private static function compress(string $string, int $compression): string + { + if ($compression === self::COMPRESSION_NONE) { + return $string; + } + + if ($compression === self::COMPRESSION_SNAPPY) { + throw new BadMethodCallException('SNAPPY compression not yet implemented'); + } + + if ($compression !== self::COMPRESSION_GZIP) { + throw new BadMethodCallException('Unknown compression flag: ' . $compression); + } + + return gzencode($string); + } + + /** + * @param string $data + * @param string $bytes + * @param int $compression + * @return mixed[] + * + * @throws Exception + */ + public function decodeString(string $data, string $bytes, int $compression = self::COMPRESSION_NONE): array + { + $offset = $bytes === self::BIT_B32 ? 4 : 2; + $packLen = self::unpack($bytes, substr($data, 0, $offset)); // int16 topic name length + + if ($packLen === 4294967295) { // uint32(4294967295) is int32 (-1) + $packLen = 0; + } + + if ($packLen === 0) { + return ['length' => $offset, 'data' => '']; + } + + $data = (string)substr($data, $offset, (int)$packLen); + $offset += $packLen; + + return ['length' => $offset, 'data' => self::decompress($data, $compression)]; + } + + /** + * Unpack a bit integer as big endian long + * + * @param string $type + * @param string $bytes + * @return mixed + * @throws Exception + */ + public static function unpack(string $type, string $bytes) + { + self::checkLen($type, $bytes); + + if ($type === self::BIT_B64) { + $set = unpack($type, $bytes); + $result = ($set[1] & 0xFFFFFFFF) << 32 | ($set[2] & 0xFFFFFFFF); + } elseif ($type === self::BIT_B16_SIGNED) { + // According to PHP docs: 's' = signed short (always 16 bit, machine byte order) + // So lets unpack it.. + $set = unpack($type, $bytes); + + // But if our system is little endian + if (self::isSystemLittleEndian()) { + // We need to flip the endianess because coming from kafka it is big endian + $set = self::convertSignedShortFromLittleEndianToBigEndian(/** @scrutinizer ignore-type */ $set); + } + $result = $set; + } else { + $result = unpack($type, $bytes); + } + + return is_array($result) ? array_shift($result) : $result; + } + + /** + * check unpack bit is valid + * + * @param string $type + * @param string $bytes + * @throws Exception + */ + protected static function checkLen(string $type, string $bytes): void + { + $expectedLength = 0; + + switch ($type) { + case self::BIT_B64: + $expectedLength = 8; + break; + case self::BIT_B32: + $expectedLength = 4; + break; + case self::BIT_B16_SIGNED: + case self::BIT_B16: + $expectedLength = 2; + break; + case self::BIT_B8: + $expectedLength = 1; + break; + } + + $length = strlen($bytes); + + if ($length !== $expectedLength) { + throw new Exception('unpack failed. string(raw) length is ' . $length . ' , TO ' . $type); + } + } + + /** + * Determines if the computer currently running this code is big endian or little endian. + */ + public static function isSystemLittleEndian(): bool + { + // If we don't know if our system is big endian or not yet... + if (self::$isLittleEndianSystem === null) { + $value = unpack('L1L', pack('V', 1)); + if ($value === false) { + self::$isLittleEndianSystem = false; + } else { + [$endianTest] = array_values($value); + + self::$isLittleEndianSystem = (int)$endianTest === 1; + } + } + + return self::$isLittleEndianSystem; + } + + /** + * Converts a signed short (16 bits) from little endian to big endian. + * + * @param int[] $bits + * + * @return int[] + */ + public static function convertSignedShortFromLittleEndianToBigEndian(array $bits): array + { + $convert = function (int $bit): int { + $lsb = $bit & 0xff; + $msb = $bit >> 8 & 0xff; + $bit = $lsb << 8 | $msb; + + if ($bit >= 32768) { + $bit -= 65536; + } + + return $bit; + }; + + return array_map($convert, $bits); + } + + private static function decompress(string $string, int $compression): string + { + if ($compression === self::COMPRESSION_NONE) { + return $string; + } + + if ($compression === self::COMPRESSION_SNAPPY) { + throw new BadMethodCallException('SNAPPY compression not yet implemented'); + } + + if ($compression !== self::COMPRESSION_GZIP) { + throw new BadMethodCallException('Unknown compression flag: ' . $compression); + } + + return gzdecode($string); + } + + /** + * @param string $data + * @param callable $func + * @param mixed|null $options + * + * @return mixed[] + * + * @throws Exception + */ + public function decodeArray(string $data, callable $func, $options = null): array + { + $offset = 0; + $arrayCount = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + + $result = []; + + for ($i = 0; $i < $arrayCount; $i++) { + $value = substr($data, $offset); + $ret = $options !== null ? $func($value, $options) : $func($value); + + if (!is_array($ret) && $ret === false) { + break; + } + + if (!isset($ret['length'], $ret['data'])) { + throw new Exception('Decode array failed, given function return format is invalid'); + } + if ((int)$ret['length'] === 0) { + continue; + } + + $offset += $ret['length']; + $result[] = $ret['data']; + } + + return ['length' => $offset, 'data' => $result]; + } + + /** + * @param string $data + * @param string $bit + * @return mixed[] + * + */ + public function decodePrimitiveArray(string $data, string $bit): array + { + $offset = 0; + $arrayCount = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + + if ($arrayCount === 4294967295) { + $arrayCount = 0; + } + + $result = []; + + for ($i = 0; $i < $arrayCount; $i++) { + if ($bit === self::BIT_B64) { + $result[] = self::unpack(self::BIT_B64, substr($data, $offset, 8)); + $offset += 8; + } elseif ($bit === self::BIT_B32) { + $result[] = self::unpack(self::BIT_B32, substr($data, $offset, 4)); + $offset += 4; + } elseif (in_array($bit, [self::BIT_B16, self::BIT_B16_SIGNED], true)) { + $result[] = self::unpack($bit, substr($data, $offset, 2)); + $offset += 2; + } elseif ($bit === self::BIT_B8) { + $result[] = self::unpack($bit, substr($data, $offset, 1)); + ++$offset; + } + } + + return ['length' => $offset, 'data' => $result]; + } + + /** + * @param array $payloads + * @return string + */ + abstract public function encode(array $payloads = []): string; + + /** + * @param string $data + * @return array + */ + abstract public function decode(string $data): array; +} diff --git a/src/Kafka/ProtocolTool.php b/src/Kafka/ProtocolTool.php new file mode 100644 index 0000000..0da4642 --- /dev/null +++ b/src/Kafka/ProtocolTool.php @@ -0,0 +1,66 @@ + Produce::class, + Protocol::METADATA_REQUEST => Metadata::class, + ]; + + foreach ($class as $key => $className) { + self::$objects[$key] = new $className($version); + } + } + + /** + * @param int $key + * @param array $payloads + * @return string + * @throws Exception + */ + public static function encode(int $key, array $payloads): string + { + if (!isset(self::$objects[$key])) { + throw new NotSupportedException('Not support api key, key:' . $key); + } + + return self::$objects[$key]->encode($payloads); + } + + /** + * @param int $key + * @param string $data + * @return array + * @throws Exception + */ + public static function decode(int $key, string $data): array + { + if (!isset(self::$objects[$key])) { + throw new NotSupportedException('Not support api key, key:' . $key); + } + + return self::$objects[$key]->decode($data); + } +} diff --git a/src/Kafka/RecordValidator.php b/src/Kafka/RecordValidator.php new file mode 100644 index 0000000..f70719b --- /dev/null +++ b/src/Kafka/RecordValidator.php @@ -0,0 +1,49 @@ + $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } + } + $this->queue = new SplQueue(); + $this->waitStack = new SplQueue(); + } + + /** + * @param SocketIO $connection + */ + public function release(SocketIO $connection) + { + if ($this->queue->count() < $this->active) { + $this->queue->push($connection); + if ($this->waitStack->count() > 0) { + $id = $this->waitStack->shift(); + Co::resume($id); + } + } + } + + /** + * @return SocketIO + * @throws Exception + */ + public function getConnection(): SocketIO + { + if (!$this->queue->isEmpty()) { + return $this->queue->shift(); + } + + if ($this->currentCount >= $this->active) { + if ($this->maxWait > 0 && $this->waitStack->count() > $this->maxWait) { + throw new Exception('Connection pool queue is full'); + } + $this->waitStack->push((int)Co::getCid()); + Co::yield(); + return $this->queue->shift(); + } + + $connection = $this->createConnection(); + $this->currentCount++; + if ($connection->check() === false) { + $connection->reconnect(); + } + return $connection; + } + + /** + * @return SocketIO + * @throws Exception + */ + public function createConnection(): SocketIO + { + $socket = new SocketIO(); + $socket->createConnection([ + 'uri' => $this->uri, + 'retry' => $this->retry, + 'sleep' => $this->waitReconnect, + 'timeout' => $this->timeout + ]); + return $socket; + } +} \ No newline at end of file diff --git a/src/Kafka/Socket/SocketIO.php b/src/Kafka/Socket/SocketIO.php new file mode 100644 index 0000000..edc7638 --- /dev/null +++ b/src/Kafka/Socket/SocketIO.php @@ -0,0 +1,106 @@ + 0) { + $result = $this->connection->sendAll($data, $timeout); + if (!is_int($result)) { + $this->reconnect(); + } + $data = substr($data, $result); + } + $this->recv = false; + return $ln; + } + + /** + * @throws Exception + */ + public function reconnect(): void + { + $this->createConnection(); + } + + /** + * @param array $config + * @throws Exception + */ + public function createConnection(array $config = []): void + { + !empty($config) && ($this->config = $config); + $client = new Socket(AF_INET, SOCK_STREAM, 0); + list($host, $port) = explode(':', $this->config['uri']); + $maxRetry = $this->config['retry']; + $reconnectCount = 0; + while (true) { + $isConnect = $this->config['timeout'] ? $client->connect($host, (int)$port, + $this->config['timeout']) : $client->connect($host, (int)$port); + if (!$isConnect) { + $reconnectCount++; + if ($maxRetry > 0 && $reconnectCount >= $maxRetry) { + $error = sprintf('Service connect fail error=%s host=%s port=%s', socket_strerror($client->errCode), + $host, $port); + throw new Exception($error); + } + Co::sleep($this->config['sleep']); + } else { + break; + } + } + $this->connection = $client; + } + + /** + * @param int $length + * @param float $timeout + * @return string + */ + public function recv(int $length = 65535, float $timeout = -1): string + { + $data = $this->connection->recvAll($length, $timeout); + return $data; + } + + /** + * @return bool + */ + public function check(): bool + { + return $this->connection->errCode === 0; + } + + /** + * @return bool + */ + public function close(): bool + { + return $this->connection->close(); + } +} \ No newline at end of file diff --git a/src/Logger.php b/src/Logger.php index 17452c1..bd54a13 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -1,5 +1,5 @@ 'ALERT', self::EMERGENCY => 'EMERGENCY', ]; + /** @var AbstractConfig */ + protected $config; /** - * set request level for seaslog. - * - * @param int $level - */ - public function setRequestLevel($level = self::ALL) - { - self::$RequestLevel = $level; - } - - /** - * @param string $message - * @param array $context - */ - public function emergency($message, array $context = []) - { - SeasLog::emergency($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function alert($message, array $context = []) - { - SeasLog::alert($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function critical($message, array $context = []) - { - SeasLog::critical($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function error($message, array $context = []) - { - SeasLog::error($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function warning($message, array $context = []) - { - SeasLog::warning($message, $context); - } - - /** - * @param string $message - * @param array $context + * Logger constructor. + * @param AbstractConfig|null $config */ - public function notice($message, array $context = []) + public function __construct(AbstractConfig $config = null) { - SeasLog::notice($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function info($message, array $context = []) - { - SeasLog::info($message, $context); - } - - /** - * @param string $message - * @param array $context - */ - public function debug($message, array $context = []) - { - SeasLog::debug($message, $context); - } - - /** - * @param mixed $level - * @param string $message - * @param array $context - */ - public function log($level, $message, array $context = []) - { - if ((int)$level < self::$RequestLevel) { - return; - } - - if (!array_key_exists($level, self::$levels)) { - return; + if ($config !== null && !extension_loaded('swoole')) { + throw new NotSupportedException("This usage must have swoole version>=4"); } - - $levelFunction = strtolower(self::$levels[$level]); - - SeasLog::$levelFunction($message, $context); + $this->config = $config; } /** - * @param string $basePath - * - * @return bool + * @return AbstractConfig|null */ - public function setBasePath(string $basePath) + public function getConfig(): ?AbstractConfig { - return SeasLog::setBasePath($basePath); + return $this->config; } /** - * @return string + * @param AbstractConfig $config + * @return Logger */ - public function getBasePath() + public function setConfig(?AbstractConfig $config): self { - return SeasLog::getBasePath(); + $this->config = $config; + return $this; } /** @@ -271,6 +179,23 @@ public static function setDatetimeFormat($format) return SeasLog::setDatetimeFormat($format); } + /** + * @param string $format + * @return bool + */ + public function setConfigDatetimeFormat(string $format): bool + { + return $this->config->setDatetimeFormat($format); + } + + /** + * @return string + */ + public function getConfigDatetimeFormat(): string + { + return $this->config->getDatetimeFormat(); + } + /** * 返回当前DatetimeFormat配置格式. * @@ -292,7 +217,7 @@ public static function getDatetimeFormat() */ public static function analyzerCount($level = 'all', $log_path = '*', $key_word = null) { - return SeasLog::analyzerCount($level, $log_path, $key_word); + return SeasLog::analyzerCount($level, $log_path, (string)$key_word); } /** @@ -318,7 +243,7 @@ public static function analyzerDetail( return SeasLog::analyzerDetail( $level, $log_path, - $key_word, + (string)$key_word, $start, $limit, $order @@ -335,6 +260,14 @@ public static function getBuffer() return SeasLog::getBuffer(); } + /** + * @return array + */ + public function getConfigBuffer(): array + { + return $this->config->getBuffer(); + } + /** * 将buffer中的日志立刻刷到硬盘. * @@ -345,6 +278,193 @@ public static function flushBuffer() return SeasLog::flushBuffer(); } + public function flushConfigBuffer(): void + { + $this->config->flush(true); + } + + /** + * Manually release stream flow from logger + * + * @param $type + * @param string $name + * @return bool + */ + public static function closeLoggerStream($type = SEASLOG_CLOSE_LOGGER_STREAM_MOD_ALL, $name = '') + { + if (empty($name)) { + return SeasLog::closeLoggerStream($type); + } + + return SeasLog::closeLoggerStream($type, $name); + + } + + /** + * set request level for seaslog. + * + * @param int $level + */ + public function setRequestLevel($level = self::ALL) + { + self::$RequestLevel = $level; + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + empty($this->config) ? SeasLog::emergency($message, $context) : $this->log(self::EMERGENCY, $message, + $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + public function log($level, $message, array $context = array()) + { + if ((int)$level < self::$RequestLevel) { + return; + } + + if (!array_key_exists($level, self::$levels)) { + return; + } + + if (empty($this->config)) { + $levelFunction = strtolower(self::$levels[$level]); + SeasLog::$levelFunction($message, $context); + } else { + $this->config->log(self::$levels[$level], $message, $context); + } + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + empty($this->config) ? SeasLog::alert($message, $context) : $this->log(self::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + empty($this->config) ? SeasLog::critical($message, $context) : $this->log(self::CRITICAL, $message, + $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + empty($this->config) ? SeasLog::error($message, $context) : $this->log(self::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + empty($this->config) ? SeasLog::warning($message, $context) : $this->log(self::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + empty($this->config) ? SeasLog::notice($message, $context) : $this->log(self::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + empty($this->config) ? SeasLog::info($message, $context) : $this->log(self::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + empty($this->config) ? SeasLog::debug($message, $context) : $this->log(self::DEBUG, $message, $context); + } + + /** + * @return string + */ + public function getBasePath() + { + if ($this->config instanceof LoggerConfig) { + throw new NotSupportedException(sprintf("LoggerConfig not support %s", __METHOD__)); + } + return SeasLog::getBasePath(); + } + /** * Create a custom SeasLog instance. * @@ -363,19 +483,15 @@ public function __invoke(array $config) } /** - * Manually release stream flow from logger + * @param string $basePath * - * @param $type - * @param string $name * @return bool */ - public static function closeLoggerStream($type = SEASLOG_CLOSE_LOGGER_STREAM_MOD_ALL, $name = '') + public function setBasePath(string $basePath) { - if (empty($name)) { - return SeasLog::closeLoggerStream($type); + if ($this->config instanceof LoggerConfig) { + throw new NotSupportedException(sprintf("LoggerConfig not support %s", __METHOD__)); } - - return SeasLog::closeLoggerStream($type, $name); - + return SeasLog::setBasePath($basePath); } -} +} \ No newline at end of file diff --git a/src/LoggerConfig.php b/src/LoggerConfig.php new file mode 100644 index 0000000..c03b965 --- /dev/null +++ b/src/LoggerConfig.php @@ -0,0 +1,212 @@ +template = $template; + parent::__construct($target, $configs); + } + + /** + * @return string + */ + public function getDatetimeFormat(): string + { + return $this->datetime_format; + } + + /** + * @param string $format + * @return bool + */ + public function setDatetimeFormat(string $format): bool + { + if (date($format, time()) !== false) { + $this->datetime_format = $format; + return true; + } + return false; + } + + /** + * 获得当前日志buffer中的内容. + * + * @return array + */ + public function getBuffer(): array + { + return $this->buffer; + } + + /** + * @param string $level + * @param string $message + * @param array $context + * @throws Exception + */ + public function log(string $level, string $message, array $context = []): void + { + $template = $this->getUserTemplate(); + $msg = []; + $module = ArrayHelper::getValue($context, 'module'); + foreach ($this->template as $tmp) { + switch ($tmp) { + case '%W': + $msg[] = ArrayHelper::getValue($template, $tmp, -1); + break; + case '%L': + $msg[] = $level; + break; + case '%M': + $msg[] = str_replace($this->split, ' ', empty($context) ? $message : strtr($message, $context)); + break; + case '%T': + case '%t': + if ($this->isMicroTime > 0) { + $micsec = $this->isMicroTime > 3 ? 3 : $this->isMicroTime; + $mtimestamp = sprintf("%.{$micsec}f", microtime(true)); // 带毫秒的时间戳 + $timestamp = floor(/** @scrutinizer ignore-type */ $mtimestamp); // 时间戳 + $milliseconds = round(($mtimestamp - $timestamp) * 1000); // 毫秒 + } else { + $timestamp = time(); + $milliseconds = 0; + } + if ($tmp === '%T') { + $msg[] = date($this->datetime_format, (int)$timestamp) . '.' . (int)$milliseconds; + } else { + $msg[] = date($this->datetime_format, (int)$timestamp); + } + break; + case '%Q': + $msg[] = ArrayHelper::getValue($template, $tmp, uniqid()); + break; + case '%H': + $msg[] = ArrayHelper::getValue($template, $tmp, $_SERVER['HOSTNAME']); + break; + case '%P': + $msg[] = ArrayHelper::getValue($template, $tmp, getmypid()); + break; + case '%D': + $msg[] = ArrayHelper::getValue($template, $tmp, 'cli'); + break; + case '%R': + $msg[] = ArrayHelper::getValue($template, $tmp, $_SERVER['SCRIPT_NAME']); + break; + case '%m': + $method = ArrayHelper::getValue($template, $tmp); + $msg[] = $method ? strtoupper($method) : $_SERVER['SHELL']; + break; + case '%I': + $msg[] = ArrayHelper::getValue($template, $tmp, 'local'); + break; + case '%F': + case '%C': + $trace = Co::getBackTrace(Co::getCid(), /** @scrutinizer ignore-type */ DEBUG_BACKTRACE_IGNORE_ARGS, + $this->recall_depth + 2); + if ($tmp === '%F') { + $trace = $trace[$this->recall_depth]; + $msg[] = $this->useBasename ? basename($trace['file']) . ':' . $trace['line'] : $trace['file'] . ':' . $trace['line']; + } else { + $trace = $trace[$this->recall_depth + 1]; + $msg[] = $trace['class'] . $trace['type'] . $trace['function']; + } + break; + case '%U': + $msg[] = memory_get_usage(); + break; + case '%u': + $msg[] = memory_get_peak_usage(); + break; + case '%A': + $customerTemplate = ArrayHelper::getValue($context, 'template', + []) ?? ArrayHelper::getValue($template, $tmp, + []); + switch ($this->customerType) { + case AbstractConfig::TYPE_JSON: + $msg[] = json_encode($customerTemplate, JSON_UNESCAPED_UNICODE); + break; + case AbstractConfig::TYPE_FIELD: + default: + $msg[] = implode($this->split, $customerTemplate); + } + break; + } + } + $color = ArrayHelper::getValue($template, '%c'); + $color && $msg['%c'] = $color; + $key = $this->appName . ($module ? '_' . $module : ''); + $this->buffer[$key][] = $msg; + $this->flush(); + } + + /** + * @param bool $flush + * @throws Exception + */ + public function flush(bool $flush = false): void + { + if (!empty($this->buffer) && $flush || ($this->bufferSize !== 0 && $this->bufferSize <= count($this->buffer))) { + foreach ($this->targetList as $index => $target) { + rgo(function () use ($target, $flush) { + $target->export($this->buffer); + }); + } + array_splice($this->buffer, 0); + } + } +} \ No newline at end of file diff --git a/src/SeaslogConfig.php b/src/SeaslogConfig.php new file mode 100644 index 0000000..02c1181 --- /dev/null +++ b/src/SeaslogConfig.php @@ -0,0 +1,104 @@ +template = explode($this->split, ini_get('seaslog.default_template')); + ini_set('seaslog.recall_depth', (string)$this->recall_depth); + parent::__construct($target, $configs); + } + + /** + * @return array + */ + public function getBuffer(): array + { + $buffer = Seaslog::getBuffer(); + return $buffer !== false ? $buffer : []; + } + + public function getDatetimeFormat(): string + { + return SeasLog::getDatetimeFormat(); + } + + public function setDatetimeFormat(string $format): bool + { + return SeasLog::setDatetimeFormat($format); + } + + + /** + * @param string $level + * @param string $message + * @param array $context + * @throws Exception + */ + public function log(string $level, string $message, array $context = []): void + { + $template = $this->getUserTemplate(); + $module = ArrayHelper::remove($context, 'module'); + if ($module !== null) { + Seaslog::setLogger($this->appName . '_' . $module); + } + isset($template['%Q']) && Seaslog::setRequestID($template['%Q']); + foreach (array_filter([ + SEASLOG_REQUEST_VARIABLE_DOMAIN_PORT => isset($template['%D']) ? $template['%D'] : null, + SEASLOG_REQUEST_VARIABLE_REQUEST_URI => isset($template['%R']) ? $template['%R'] : null, + SEASLOG_REQUEST_VARIABLE_REQUEST_METHOD => isset($template['%m']) ? $template['%m'] : null, + SEASLOG_REQUEST_VARIABLE_CLIENT_IP => isset($template['%I']) ? $template['%I'] : null + ]) as $key => $value) { + Seaslog::setRequestVariable($key, $value); + } + if (!empty($template = ArrayHelper::remove($context, 'template', []) ?? ArrayHelper::remove($template, '%A', + []))) { + switch ($this->customerType) { + case AbstractConfig::TYPE_JSON: + $template = json_encode($template, JSON_UNESCAPED_UNICODE); + break; + case AbstractConfig::TYPE_FIELD: + $template = implode($this->split, $template); + } + $message = $template . $this->split . $message; + } + Seaslog::$level($message); + $this->flush(); + } + + /** + * @param bool $flush + * @throws Exception + */ + public function flush(bool $flush = false): void + { + if (method_exists('Seaslog', 'getBufferCount')) { + if (($flush || Seaslog::getBufferCount() >= $this->bufferSize) && ($buffer = Seaslog::getBuffer()) !== false) { + Seaslog::flushBuffer(0); + foreach ($this->targetList as $index => $target) { + rgo(function () use ($target, $buffer) { + $target->export($buffer); + }); + } + unset($buffer); + } + } elseif ($flush) { + Seaslog::flushBuffer(); + } + } +} \ No newline at end of file diff --git a/src/Targets/AbstractTarget.php b/src/Targets/AbstractTarget.php new file mode 100644 index 0000000..7fee071 --- /dev/null +++ b/src/Targets/AbstractTarget.php @@ -0,0 +1,92 @@ +levelList = $levelList; + } + + /** + * @param string $split + * @return AbstractTarget + */ + public function setSplit(string $split): self + { + $this->split = $split; + return $this; + } + + /** + * @param array $template + * @return AbstractTarget + */ + public function setTemplate(array $template): self + { + $this->template = $template; + return $this; + } + + /** + * @param string $type + * @return AbstractTarget + */ + public function setCustomerFieldType(string $type): self + { + if (!in_array($type, AbstractConfig::getSupportFieldType())) { + throw new NotSupportedException("The field type not support $type"); + } + if ($this->customerType === null) { + $this->customerType = $type; + } + + return $this; + } + + /** + * @param array $messages + */ + abstract public function export(array $messages): void; + + /** + * @param string $str + * @param string $find + * @param int $n + * @return int + */ + protected function str_n_pos(string $str, string $find, int $n): int + { + $pos_val = 0; + for ($i = 1; $i <= $n; $i++) { + $pos = strpos($str, $find); + $str = substr($str, $pos + 1); + $pos_val = $pos + $pos_val + 1; + } + return $pos_val - 1; + } +} \ No newline at end of file diff --git a/src/Targets/EchoTarget.php b/src/Targets/EchoTarget.php new file mode 100644 index 0000000..8ef38e2 --- /dev/null +++ b/src/Targets/EchoTarget.php @@ -0,0 +1,32 @@ +split, trim($msg)); + } + if (!empty($this->levelList) && !in_array(strtolower($msg[$this->levelIndex]), $this->levelList)) { + continue; + } + ArrayHelper::remove($msg, '%c'); + echo implode($this->split, $msg) . PHP_EOL; + } + } + } +} \ No newline at end of file diff --git a/src/Targets/KafkaTarget.php b/src/Targets/KafkaTarget.php new file mode 100644 index 0000000..d5f53ea --- /dev/null +++ b/src/Targets/KafkaTarget.php @@ -0,0 +1,141 @@ +client = $client; + $this->topic = $topic; + $this->fieldTemplate = $fieldTemplate; + $this->customerTmp = $customerTmp; + $this->levelList = $levelList; + } + + /** + * @param array $messages + * @throws Exception + */ + public function export(array $messages): void + { + foreach ($messages as $module => $message) { + foreach ($message as $msg) { + if (is_string($msg)) { + switch (ini_get('seaslog.appender')) { + case '2': + case '3': + $msg = trim(substr($msg, $this->str_n_pos($msg, ' ', 6))); + break; + case '1': + default: + $fileName = basename($module); + $module = substr($fileName, 0, strrpos($fileName, '_')); + } + $msg = explode($this->split, trim($msg)); + } else { + ArrayHelper::remove($msg, '%c'); + } + if (!empty($this->levelList) && !in_array(strtolower($msg[$this->levelIndex]), $this->levelList)) { + continue; + } + $log = [ + 'appname' => $module, + ]; + $i = 0; + foreach ($msg as $index => $msgValue) { + if ($this->template[$index] === '%A') { + switch ($this->customerType) { + case AbstractConfig::TYPE_JSON: + $msgValue = json_decode($msgValue, true); + break; + case AbstractConfig::TYPE_FIELD: + default: + $msgValue = explode($this->split, $msgValue); + } + foreach ($this->customerTmp as $tmpIndex => [$name, $type]) { + $this->makeLog($log, $name, $type, isset($msgValue[$tmpIndex]) ? $msgValue[$tmpIndex] : ''); + } + } else { + [$name, $type] = $this->fieldTemplate[$i]; + $this->makeLog($log, $name, $type, $msgValue); + $i++; + } + } + $this->client->send([ + [ + 'topic' => $this->topic, + 'value' => json_encode($log), + 'key' => '' + ] + ]); + } + } + } + + /** + * @param array $log + * @param string $name + * @param string $type + * @param $value + */ + private function makeLog(array &$log, string $name, string $type, $value): void + { + switch ($type) { + case "timespan": + $log[$name] = $value ? strtotime(explode('.', $value)[0]) : 0; + break; + case "int": + $log[$name] = $value ? (int)$value : 0; + break; + case "string": + $log[$name] = $value ? trim($value) : ''; + break; + default: + $log[$type][$name] = $value; + } + } +} \ No newline at end of file diff --git a/src/Targets/SeasStashTarget.php b/src/Targets/SeasStashTarget.php new file mode 100644 index 0000000..495e719 --- /dev/null +++ b/src/Targets/SeasStashTarget.php @@ -0,0 +1,65 @@ +clientPool = $clientPool; + $this->levelList = $levelList; + } + + /** + * @param array $messages + * @throws Exception + */ + public function export(array $messages): void + { + $connection = $this->clientPool->getConnection(); + foreach ($messages as $module => $message) { + foreach ($message as $msg) { + if (is_string($msg)) { + switch (ini_get('seaslog.appender')) { + case '2': + case '3': + $msg = trim(substr($msg, $this->str_n_pos($msg, ' ', 6))); + break; + case '1': + default: + $fileName = basename($module); + $module = substr($fileName, 0, strpos($fileName, '_', -1)); + } + $msg = explode($this->split, trim($msg)); + if (($diff = count($msg) - count($this->template)) > 0) { + array_splice($msg, -($diff + 1), $diff, [array_slice($msg, -($diff + 1), $diff)]); + } + } + if (!empty($this->levelList) && !in_array(strtolower($msg[$this->levelIndex]), $this->levelList)) { + continue; + } + ArrayHelper::remove($msg, '%c'); + $msg = $module . '@' . str_replace(PHP_EOL, '', implode($this->split, $msg)) . PHP_EOL; + $connection->send($msg); + } + } + $this->clientPool->release($connection); + } +} \ No newline at end of file diff --git a/src/Targets/StyleTarget.php b/src/Targets/StyleTarget.php new file mode 100644 index 0000000..7a06431 --- /dev/null +++ b/src/Targets/StyleTarget.php @@ -0,0 +1,131 @@ +color = $color; + $this->levelList = $levelList; + } + + /** + * @param array $messages + */ + public function export(array $messages): void + { + foreach ($messages as $message) { + foreach ($message as $msg) { + if (is_string($msg)) { + switch (ini_get('seaslog.appender')) { + case '2': + case '3': + $msg = trim(substr($msg, $this->str_n_pos($msg, ' ', 6))); + break; + } + $msg = explode($this->split, trim($msg)); + $ranColor = $this->default; + } else { + $ranColor = ArrayHelper::remove($msg, '%c'); + } + if (!empty($this->levelList) && !in_array(strtolower($msg[$this->levelIndex]), $this->levelList)) { + continue; + } + if (empty($ranColor)) { + $ranColor = $this->default; + } elseif (is_array($ranColor) && isset($ranColor['console'])) { + $ranColor = $ranColor['console']; + } else { + $ranColor = $this->default; + } + $context = []; + foreach ($msg as $index => $msgValue) { + $level = $this->getLevelColor(trim($msg[$this->levelIndex])); + if (isset($this->colorTemplate[$index])) { + $color = $this->colorTemplate[$index]; + $msgValue = is_string($msgValue) ? trim($msgValue) : (string)$msgValue; + switch ($color) { + case self::COLOR_LEVEL: + $context[] = $this->color->getColoredString($msgValue, $level); + break; + case self::COLOR_DEFAULT: + $context[] = $this->color->getColoredString($msgValue, $this->default); + break; + case self::COLOR_RANDOM: + $context[] = $this->color->getColoredString($msgValue, $ranColor); + break; + default: + $context[] = $this->color->getColoredString($msgValue, $color); + } + } else { + $context[] = $this->color->getColoredString($msgValue, $level); + } + } + if (isset($context)) { + echo implode(' ' . $this->color->getColoredString('|', $this->splitColor) . ' ', + $context) . PHP_EOL; + } + } + } + } + + /** + * @param string $level + * @return string + */ + private function getLevelColor(string $level): string + { + switch (strtolower($level)) { + case LogLevel::INFO: + return "green"; + case LogLevel::DEBUG: + return 'dark_gray'; + case LogLevel::ERROR: + return "red"; + case LogLevel::WARNING: + return 'yellow'; + default: + return 'light_red'; + } + } + +} \ No newline at end of file diff --git a/src/Targets/WebsocketTarget.php b/src/Targets/WebsocketTarget.php new file mode 100644 index 0000000..316bc7c --- /dev/null +++ b/src/Targets/WebsocketTarget.php @@ -0,0 +1,142 @@ +getServer = $function; + } + + /** + * @param array $messages + * @throws Exception + */ + public function export(array $messages): void + { + if (!is_callable($this->getServer)) { + return; + } + /** @var Server $server */ + $swooleServer = call_user_func($this->getServer); + if (!$swooleServer || !$swooleServer instanceof Server) { + return; + } + foreach ($swooleServer->connections as $fd) { + if ($swooleServer->isEstablished($fd)) { + foreach ($messages as $message) { + foreach ($message as $msg) { + if (is_string($msg)) { + switch (ini_get('seaslog.appender')) { + case '2': + case '3': + $msg = trim(substr($msg, $this->str_n_pos($msg, ' ', 6))); + break; + } + $msg = explode($this->split, trim($msg)); + $ranColor = $this->default; + } else { + $ranColor = ArrayHelper::remove($msg, '%c'); + } + if (!empty($this->levelList) && !in_array(strtolower($msg[$this->levelIndex]), + $this->levelList)) { + continue; + } + if (empty($ranColor)) { + $ranColor = $this->default; + } elseif (is_array($ranColor) && isset($ranColor['websocket'])) { + $ranColor = $ranColor['websocket']; + } else { + $ranColor = $this->default; + } + $colors = []; + foreach ($msg as $index => $msgValue) { + $msg[$index] = is_string($msgValue) ? trim($msgValue) : (string)$msgValue; + $level = $this->getLevelColor(trim($msg[$this->levelIndex])); + if (isset($this->colorTemplate[$index])) { + $color = $this->colorTemplate[$index]; + switch ($color) { + case self::COLOR_LEVEL: + $colors[] = HtmlColor::getColor($level); + break; + case self::COLOR_RANDOM: + $colors[] = HtmlColor::getColor($ranColor); + break; + case self::COLOR_DEFAULT: + $colors[] = $this->default; + break; + default: + $colors[] = HtmlColor::getColor($color); + } + } else { + $colors[] = $level; + } + } + $msg = json_encode([$msg, $colors], JSON_UNESCAPED_UNICODE); + rgo(function () use ($swooleServer, $fd, $msg) { + $swooleServer->push($fd, $msg); + }); + } + } + } + } + } + + /** + * @param string $level + * @return string + */ + private function getLevelColor(string $level): string + { + switch (strtolower($level)) { + case LogLevel::INFO: + return "Green"; + case LogLevel::DEBUG: + return 'DarkGray'; + case LogLevel::ERROR: + return "Red"; + case LogLevel::WARNING: + return 'Yellow'; + default: + return 'DarkRed'; + } + } + +} \ No newline at end of file diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..4b6bc27 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,30 @@ +getTraceAsString()); + return 0; + } + }); + if (is_int($cid)) { + return $cid; + } + return 0; + } +} \ No newline at end of file diff --git a/tests/ArrayHelperTest.php b/tests/ArrayHelperTest.php new file mode 100644 index 0000000..2517324 --- /dev/null +++ b/tests/ArrayHelperTest.php @@ -0,0 +1,35 @@ +assertEquals('test', ArrayHelper::getValue([ + 'key' => 'test' + ], 'key')); + } + + public function testGetValueDefault() + { + $this->assertEquals('test', ArrayHelper::getValue([ + ], 'key', 'test')); + } + + public function testRemove() + { + $value = [ + 'key' => 'test' + ]; + $this->assertEquals('test', ArrayHelper::remove($value, 'key')); + $this->assertArrayNotHasKey('test', $value); + } +} \ No newline at end of file diff --git a/tests/ContextTest.php b/tests/ContextTest.php new file mode 100644 index 0000000..fb2ca6d --- /dev/null +++ b/tests/ContextTest.php @@ -0,0 +1,43 @@ +assertEquals('value', Context::get('key')); + }); + } + + public function testHas() + { + \Co\Run(function () { + Context::set('key', 'value'); + $this->assertTrue(Context::has('key')); + }); + } + + public function testNotHas() + { + \Co\Run(function () { + $this->assertFalse(Context::has('key')); + }); + } + + public function testDelete() + { + \Co\Run(function () { + Context::set('key', 'value'); + $this->assertEquals('value', Context::get('key')); + Context::delete('key'); + $this->assertFalse(Context::has('key')); + }); + } +} \ No newline at end of file diff --git a/tests/EchoTargetTest.php b/tests/EchoTargetTest.php new file mode 100644 index 0000000..0aed724 --- /dev/null +++ b/tests/EchoTargetTest.php @@ -0,0 +1,131 @@ +assertInstanceOf(EchoTarget::class, $target); + $target->export([ + 'logger' => [ + implode(' | ', [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[SeasLog String]' + ]) + ] + ]); + } + + public function testExport() + { + $target = new EchoTarget(); + $this->assertInstanceOf(EchoTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithCustomerTemplate() + { + $target = new EchoTarget(); + $this->assertInstanceOf(EchoTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + implode(' | ', ['123']), + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithCustomerTypeJson() + { + $target = new EchoTarget(); + $target->setCustomerFieldType(AbstractConfig::TYPE_JSON); + $this->assertInstanceOf(EchoTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + json_encode(['task_id' => '123']), + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithLevel() + { + $target = new EchoTarget([ + 'info' + ]); + $this->assertInstanceOf(EchoTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[Test WARNING]' + ], + [ + '2019-08-30 09:58:01.937', + 'INFO', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[Test INFO]' + ] + ] + ]); + } +} \ No newline at end of file diff --git a/tests/HtmlColorTest.php b/tests/HtmlColorTest.php new file mode 100644 index 0000000..55d1d97 --- /dev/null +++ b/tests/HtmlColorTest.php @@ -0,0 +1,23 @@ +assertEquals(count(HtmlColor::getPossibleColors()), count(HtmlColor::getPossibleColorsRGB())); + } + + public function testGetColor() + { + $keys = HtmlColor::getPossibleColors(); + $values = HtmlColor::getPossibleColorsRGB(); + $index = rand(0, count($keys)); + $this->assertEquals(HtmlColor::getColor($keys[$index]), $values[$index]); + } +} \ No newline at end of file diff --git a/tests/LoggerConfigTest.php b/tests/LoggerConfigTest.php new file mode 100644 index 0000000..5128aa1 --- /dev/null +++ b/tests/LoggerConfigTest.php @@ -0,0 +1,60 @@ + 'Seaslog', + 'bufferSize' => $bufferSize, + 'tick' => 0, + 'recall_depth' => 1, + ]); + return $config; + } + + public function testSetDatetimeFormat() + { + $config = $this->init(); + $result = $config->setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + } + + public function testGetDatetimeFormat() + { + $config = $this->init(); + $result = $config->setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + $format = $config->getDatetimeFormat(); + $this->assertEquals('Y-m-d H:i:s', $format); + } + + public function testGetBuffer() + { + $config = $this->init(100); + $config->log(LogLevel::INFO, 'LoggerConfig Test'); + $buffer = $config->getBuffer(); + $this->assertNotEmpty($buffer); + $this->assertEquals(count($buffer['Seaslog']), 1); + $this->assertEquals(count(current($buffer['Seaslog'])), count($config->getTemplate())); + } + + public function testFlush() + { + \Co\Run(function () { + $config = $this->init(100); + $config->log(LogLevel::INFO, 'LoggerConfig Test'); + $config->flush(true); + $this->assertEmpty($config->getBuffer()); + }); + } +} \ No newline at end of file diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php index ab2e7f4..e9c758b 100644 --- a/tests/LoggerTest.php +++ b/tests/LoggerTest.php @@ -1,5 +1,5 @@ init(); + + $basePath = $logger->getBasePath(); + $this->assertEquals('/tmp/seaslogger', $basePath); + } + /** * @return Logger */ @@ -26,14 +34,6 @@ public function init() return $logger; } - public function testGetBasePath() - { - $logger = $this->init(); - - $basePath = $logger->getBasePath(); - $this->assertEquals('/tmp/seaslogger', $basePath); - } - public function testSetRequestID() { $logger = $this->init(); diff --git a/tests/NotSupportedExceptionTest.php b/tests/NotSupportedExceptionTest.php new file mode 100644 index 0000000..0ff5893 --- /dev/null +++ b/tests/NotSupportedExceptionTest.php @@ -0,0 +1,29 @@ + | + +----------------------------------------------------------------------+ + */ + +namespace Seasx\SeasLogger\Tests; + +use Swoole; +use swoole_atomic; + +class ProcessManager +{ + public $parentFunc; + public $childFunc; + /** + * @var swoole_atomic + */ + protected $atomic; + /** + * wait wakeup 1s default + */ + protected $waitTimeout = 1.0; + protected $childPid; + protected $childStatus = 255; + protected $parentFirst = false; + /** + * @var Swoole\Process + */ + protected $childProcess; + + public function __construct() + { + $this->atomic = new Swoole\Atomic(0); + } + + //等待信息 + + public function wakeup() + { + return $this->atomic->wakeup(); + } + + //唤醒等待的进程 + + public function run($redirectStdout = false) + { + $this->childProcess = new Swoole\Process(function () { + if ($this->parentFirst) { + $this->wait(); + } + $this->runChildFunc(); + exit; + }, $redirectStdout, $redirectStdout); + if (!$this->childProcess || !$this->childProcess->start()) { + exit("ERROR: CAN NOT CREATE PROCESS\n"); + } + register_shutdown_function(function () { + $this->kill(); + }); + if (!$this->parentFirst) { + $this->wait(); + } + $this->runParentFunc($this->childPid = $this->childProcess->pid); + Swoole\Event::wait(); + $waitInfo = Swoole\Process::wait(true); + $this->childStatus = $waitInfo['code']; + return true; + } + + public function wait() + { + return $this->atomic->wait($this->waitTimeout); + } + + public function runChildFunc() + { + return call_user_func($this->childFunc); + } + + /** + * Kill Child Process + * @param bool $force + */ + public function kill(bool $force = false) + { + if (!defined('PCNTL_ESRCH')) { + define('PCNTL_ESRCH', 3); + } + if ($this->childPid) { + if ($force || (!@Swoole\Process::kill($this->childPid) && swoole_errno() !== PCNTL_ESRCH)) { + if (!@Swoole\Process::kill($this->childPid, SIGKILL) && swoole_errno() !== PCNTL_ESRCH) { + exit('KILL CHILD PROCESS ERROR'); + } + } + } + } + + public function runParentFunc($pid = 0) + { + if (!$this->parentFunc) { + return (function () { + $this->kill(); + })(); + } else { + return call_user_func($this->parentFunc, $pid); + } + } + + public function getChildOutput() + { + $this->childProcess->setBlocking(false); + while (1) { + $data = @$this->childProcess->read(); + if (!$data) { + sleep(1); + } else { + return $data; + } + } + } + + /** + * @param $data + */ + public function setChildOutput($data) + { + $this->childProcess->write($data); + } +} \ No newline at end of file diff --git a/tests/SeasStashTargetTest.php b/tests/SeasStashTargetTest.php new file mode 100644 index 0000000..dfb637a --- /dev/null +++ b/tests/SeasStashTargetTest.php @@ -0,0 +1,132 @@ + [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + implode(' | ', ['123', '456']), + '[SeasLog Array]' + ] + ] + ]; + $target = new SeasStashTarget(new Pool([ + 'uri' => '127.0.0.1:9501' + ])); + $this->assertInstanceOf(SeasStashTarget::class, $target); + + $pm->parentFunc = function () use ($log, $pm, $target) { + $str = $pm->getChildOutput(); + $src = []; + foreach (current($log['logger']) as $item) { + if (is_array($item)) { + $item = implode(' | ', $item); + } + $src[] = $item; + } + $this->assertEquals(trim($str), trim('logger@' . implode(' | ', $src))); + $pm->kill(); + }; + $pm->childFunc = function () use ($target, $pm, $log) { + $server = new Server('127.0.0.1', 9501, SWOOLE_BASE); + $server->set([ + 'worker_num' => 1, + 'log_file' => '/dev/null', + 'open_eof_check' => true, + 'package_eof' => PHP_EOL, + 'pid_file' => '/dev/shm/tcp.pid' + ]); + $server->on('workerstart', function (Server $server, int $worker_id) use ($target, $pm, $log) { + $pm->wakeup(); + $target->export($log); + }); + $server->on('receive', + function (Swoole\Server $server, int $fd, int $reactor_id, string $data) use ($pm) { + $pm->setChildOutput($data); + $server->shutdown(); + }); + $server->start(); + }; + $pm->run(true); + } + + public function testExportWithJsonType() + { + $pm = new ProcessManager(); + $log = [ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + json_encode(['task_id' => '123', 'id' => 'abc']), + '[SeasLog Array]' + ] + ] + ]; + $target = new SeasStashTarget(new Pool([ + 'uri' => '127.0.0.1:9501' + ])); + $target->setCustomerFieldType(AbstractConfig::TYPE_JSON); + $this->assertInstanceOf(SeasStashTarget::class, $target); + + $pm->parentFunc = function () use ($log, $pm, $target) { + $str = $pm->getChildOutput(); + $src = []; + foreach (current($log['logger']) as $item) { + if (is_array($item)) { + $item = json_encode($item); + } + $src[] = $item; + } + $this->assertEquals(trim($str), trim('logger@' . implode(' | ', $src))); + $pm->kill(); + }; + $pm->childFunc = function () use ($target, $pm, $log) { + $server = new Server('127.0.0.1', 9501, SWOOLE_BASE); + $server->set([ + 'worker_num' => 1, + 'log_file' => '/dev/null', + 'open_eof_check' => true, + 'package_eof' => PHP_EOL, + 'pid_file' => '/dev/shm/tcp.pid' + ]); + $server->on('workerstart', function (Server $server, int $worker_id) use ($target, $pm, $log) { + $pm->wakeup(); + $target->export($log); + }); + $server->on('receive', + function (Swoole\Server $server, int $fd, int $reactor_id, string $data) use ($pm) { + $pm->setChildOutput($data); + $server->shutdown(); + }); + $server->start(); + }; + $pm->run(true); + } +} \ No newline at end of file diff --git a/tests/SeaslogConfigTest.php b/tests/SeaslogConfigTest.php new file mode 100644 index 0000000..72e5c4d --- /dev/null +++ b/tests/SeaslogConfigTest.php @@ -0,0 +1,57 @@ + 'Seaslog', + 'bufferSize' => $bufferSize, + 'tick' => 0, + 'recall_depth' => 1, + ]); + return $config; + } + + public function testSetDatetimeFormat() + { + $config = $this->init(); + $result = $config->setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + } + + public function testGetDatetimeFormat() + { + $config = $this->init(); + $result = $config->setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + $format = $config->getDatetimeFormat(); + $this->assertEquals('Y-m-d H:i:s', $format); + } + + public function testGetBuffer() + { + $config = $this->init(100); + $config->log(LogLevel::INFO, 'LoggerConfig Test'); + $buffer = $config->getBuffer(); + $this->assertNotNull($buffer); + } + + public function testFlush() + { + \Co\Run(function () { + $config = $this->init(100); + $config->log(LogLevel::INFO, 'LoggerConfig Test'); + $config->flush(true); + $this->assertEmpty($config->getBuffer()); + }); + } +} \ No newline at end of file diff --git a/tests/StyleTargetTest.php b/tests/StyleTargetTest.php new file mode 100644 index 0000000..e375e5d --- /dev/null +++ b/tests/StyleTargetTest.php @@ -0,0 +1,147 @@ +assertInstanceOf(AbstractTarget::class, $target->setCustomerFieldType(AbstractConfig::TYPE_JSON)); + } + + public function testSetNotSupportCustomerFieldType() + { + $target = new StyleTarget(); + $this->expectException(NotSupportedException::class); + $target->setCustomerFieldType('test'); + } + + public function testStringExport() + { + $target = new StyleTarget(); + $this->assertInstanceOf(StyleTarget::class, $target); + $target->export([ + 'logger' => [ + implode(' | ', [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[SeasLog String]' + ]) + ] + ]); + } + + public function testExport() + { + $target = new StyleTarget(); + $this->assertInstanceOf(StyleTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithCustomerTemplate() + { + $target = new StyleTarget(); + $this->assertInstanceOf(StyleTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + implode(' | ', ['123']), + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithCustomerTypeJson() + { + $target = new StyleTarget(); + $target->setCustomerFieldType(AbstractConfig::TYPE_JSON); + $this->assertInstanceOf(StyleTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + json_encode(['task_id' => '123']), + '[SeasLog Array]' + ] + ] + ]); + } + + public function testExportWithLevel() + { + $target = new StyleTarget([ + 'info' + ]); + $this->assertInstanceOf(StyleTarget::class, $target); + $target->export([ + 'logger' => [ + [ + '2019-08-30 09:58:01.937', + 'WARNING', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[Test WARNING]' + ], + [ + '2019-08-30 09:58:01.937', + 'INFO', + 'vendor/phpunit/phpunit/phpunit', + '/bin/bash', + 'local', + '5d6882a9e38ff', + 'Logger.php:453', + 1380624, + '[Test INFO]' + ] + ] + ]); + } +} \ No newline at end of file diff --git a/tests/SwooleLoggerTest.php b/tests/SwooleLoggerTest.php new file mode 100644 index 0000000..a37352f --- /dev/null +++ b/tests/SwooleLoggerTest.php @@ -0,0 +1,413 @@ +assertEmpty($logger->getConfig()); + + $logger = $this->init(); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + + $logger = $this->init(1); + $this->assertInstanceOf(SeaslogConfig::class, $logger->getConfig()); + } + + public function testGetBasePath() + { + $logger = $this->init(1); + $logger->setBasePath('/tmp/seaslogger'); + $basePath = $logger->getBasePath(); + $this->assertEquals('/tmp/seaslogger', $basePath); + } + + /** + * @param int $type + * @param int $bufferSize + * @return Logger + */ + public function init(int $type = 0, int $bufferSize = 1) + { + $class = $type === 0 ? LoggerConfig::class : SeaslogConfig::class; + $logger = new Logger(); + $logger->setConfig(new $class([ + 'echo' => new StyleTarget() + ], [ + 'appName' => 'Seaslog', + 'bufferSize' => $bufferSize, + 'tick' => 0, + 'recall_depth' => 2, + ])); + return $logger; + } + + public function testSetRequestID() + { + $logger = $this->init(1); + $result = $logger::setRequestID(1024); + $this->assertTrue($result); + } + + public function testGetRequestID() + { + $logger = $this->init(1); + $result = $logger::setRequestID(1024); + $this->assertTrue($result); + $requestID = $logger::getRequestID(); + $this->assertEquals(1024, $requestID); + } + + public function testEmergency() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->emergency('[LoggerConfig Test]', ['level' => 'emergency']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->emergency('[SeasLog Test]', ['level' => 'emergency']); + } + + public function testAlert() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->alert('[LoggerConfig Test]', ['level' => 'alert']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->alert('[SeasLog Test]', ['level' => 'alert']); + } + + public function testCritical() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->critical('[LoggerConfig Test]', ['level' => 'critical']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->critical('[SeasLog Test]', ['level' => 'critical']); + } + + public function testError() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->error('[LoggerConfig Test]', ['level' => 'error']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->error('[SeasLog Test]', ['level' => 'error']); + } + + public function testWarning() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->warning('[LoggerConfig Test]', ['level' => 'warning']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->warning('[SeasLog Test]', ['level' => 'warning']); + } + + public function testNotice() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->notice('[LoggerConfig Test]', ['level' => 'notice']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->notice('[SeasLog Test]', ['level' => 'notice']); + } + + public function testInfo() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->info('[LoggerConfig Test]', ['level' => 'info']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->info('[SeasLog Test]', ['level' => 'info']); + } + + public function testDebug() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->debug('[LoggerConfig Test]', ['level' => 'debug']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->debug('[SeasLog Test]', ['level' => 'debug']); + } + + public function testLog() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->log(Logger::DEBUG, '[LoggerConfig Test]', ['level' => 'log']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->log(Logger::DEBUG, '[SeasLog Test]', ['level' => 'log']); + } + + public function testLogWithFieldTemplate() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->log(Logger::INFO, '[LoggerConfig FieldTemplate]', ['level' => 'log', 'template' => ['test']]); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->log(Logger::INFO, '[SeasLog FieldTemplate]', ['level' => 'log', 'template' => ['test']]); + } + + public function testLogWithJsonTemplate() + { + $logger = new Logger(); + $logger->setConfig(new LoggerConfig([ + 'echo' => new StyleTarget() + ], [ + 'appName' => 'Seaslog', + 'customerType' => AbstractConfig::TYPE_JSON, + 'bufferSize' => 1, + 'tick' => 0, + 'recall_depth' => 2, + ])); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->log(Logger::INFO, '[LoggerConfig JsonTemplate]', + ['level' => 'log', 'template' => ['task_type' => 'test']]); + +// $logger = new Logger( +// new SeaslogConfig([ +// 'echo' => new StyleTarget() +// ], [ +// 'appName' => 'Seaslog', +// 'customerType' => AbstractConfig::TYPE_JSON, +// 'bufferSize' => 1, +// 'tick' => 0, +// 'recall_depth' => 2, +// ])); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->log(Logger::INFO, '[SeasLog JsonTemplate]', ['level' => 'log', 'template' => ['task_type' => 'test']]); + } + + public function testRequestLevel() + { + $logger = $this->init(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerConfig::class, $logger->getConfig()); + $logger->setRequestLevel(Logger::ALL); + $logger->log(Logger::DEBUG, '[LoggerConfig Test]', ['level' => 'DEBUG']); + $logger->log(Logger::WARNING, '[LoggerConfig Test]', ['level' => 'WARNING']); + $logger->log(Logger::ERROR, '[LoggerConfig Test]', ['level' => 'ERROR']); + $logger->log(Logger::INFO, '[LoggerConfig Test]', ['level' => 'INFO']); + $logger->log(Logger::CRITICAL, '[LoggerConfig Test]', ['level' => 'CRITICAL']); + $logger->log(Logger::EMERGENCY, '[LoggerConfig Test]', ['level' => 'EMERGENCY']); + $logger->log(Logger::NOTICE, '[LoggerConfig Test]', ['level' => 'NOTICE']); + $logger->log(Logger::ALERT, '[LoggerConfig Test]', ['level' => 'ALERT']); + $logger->log(0, '[LoggerConfig Test]', ['level' => 'default']); + $logger->log(Logger::ALL - 1, '[LoggerConfig Test]', ['level' => 'default']); + +// $logger = $this->init(1); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->setRequestLevel(Logger::ALL); +// $logger->log(Logger::DEBUG, '[SeasLog Test]', ['level' => 'DEBUG']); +// $logger->log(Logger::WARNING, '[SeasLog Test]', ['level' => 'WARNING']); +// $logger->log(Logger::ERROR, '[SeasLog Test]', ['level' => 'ERROR']); +// $logger->log(Logger::INFO, '[SeasLog Test]', ['level' => 'INFO']); +// $logger->log(Logger::CRITICAL, '[SeasLog Test]', ['level' => 'CRITICAL']); +// $logger->log(Logger::EMERGENCY, '[SeasLog Test]', ['level' => 'EMERGENCY']); +// $logger->log(Logger::NOTICE, '[SeasLog Test]', ['level' => 'NOTICE']); +// $logger->log(Logger::ALERT, '[SeasLog Test]', ['level' => 'ALERT']); +// $logger->log(0, '[SeasLog Test]', ['level' => 'default']); +// $logger->log(Logger::ALL - 1, '[SeasLog Test]', ['level' => 'default']); + } + + public function testSetLogger() + { + $logger = $this->init(1); + $result = $logger::setLogger('seas'); + $this->assertTrue($result); + } + + public function testGetLastLogger() + { + $logger = $this->init(1); + $result = $logger::setLogger('seas'); + $this->assertTrue($result); + $model = $logger::getLastLogger(); + $this->assertEquals('seas', $model); + } + + public function testSetDatetimeFormat() + { + $logger = $this->init(1); + $result = $logger::setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + } + + public function testSetConfigDatetimeFormat() + { + $logger = $this->init(); + $result = $logger->setConfigDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + + $logger = $this->init(1); + $result = $logger->setConfigDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + } + + public function testGetDatetimeFormat() + { + $logger = $this->init(1); + $result = $logger::setDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + $format = $logger::getDatetimeFormat(); + $this->assertEquals('Y-m-d H:i:s', $format); + } + + public function testGetConfigDatetimeFormat() + { + $logger = $this->init(); + $result = $logger->setConfigDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + $format = $logger->getConfigDatetimeFormat(); + $this->assertEquals('Y-m-d H:i:s', $format); + + $logger = $this->init(1); + $result = $logger->setConfigDatetimeFormat('Y-m-d H:i:s'); + $this->assertTrue($result); + $format = $logger->getConfigDatetimeFormat(); + $this->assertEquals('Y-m-d H:i:s', $format); + } + + public function testAnalyzerCount() + { + $logger = $this->init(1); + $result = $logger::analyzerCount(); + $this->assertNotNull($result); + } + + public function testAnalyzerDetail() + { + $logger = $this->init(1); + $result = $logger::analyzerDetail(); + $this->assertNotNull($result); + } + + public function testGetBuffer() + { + $logger = $this->init(1); + $this->assertInstanceOf(Logger::class, $logger); + $logger->info('[SeasLog Get Buffer]', ['level' => 'info']); + $buffer = $logger::getBuffer(); + $this->assertNotNull($buffer); + } + + public function testGetConfigBuffer() + { + $logger = $this->init(0, 100); + $this->assertInstanceOf(Logger::class, $logger); + $logger->info('[LoggerConfig Get Buffer]', ['level' => 'info']); + $buffer = $logger->getConfigBuffer(); + $logger->flushConfigBuffer(); + $this->assertNotNull($buffer); + +// $logger = $this->init(1, 100); +// $this->assertInstanceOf(Logger::class, $logger); +// $logger->info('[SeaslogConfig Get Buffer]', ['level' => 'info']); +// $buffer = $logger->getConfigBuffer(); +// $this->assertNotNull($buffer); + } + + public function testFlushConfigBuffer() + { + $logger = $this->init(0, 100); + $this->assertInstanceOf(Logger::class, $logger); + $logger->info('[LoggerConfig Flush]', ['level' => 'info']); + $logger->flushConfigBuffer(); + $this->assertEmpty($logger->getConfigBuffer()); + + $logger = $this->init(1); + $this->assertInstanceOf(Logger::class, $logger); + $logger->info('[SeaslogConfig Flush]', ['level' => 'info']); + $logger->flushConfigBuffer(); + $this->assertEmpty($logger->getConfigBuffer()); + } + + public function testInvoke() + { + $logger = $this->init(); + $seasLogger = $logger(['path' => '/tmp/logger']); + $this->assertInstanceOf(Logger::class, $seasLogger); + + $logger = $this->init(1); + $seasLogger = $logger(['path' => '/tmp/logger']); + $this->assertInstanceOf(Logger::class, $seasLogger); + } + + public function testCloseLoggerStreamAll() + { + $logger = $this->init(1); + $logger->setBasePath('/tmp/allLogger'); + $logger->log(Logger::DEBUG, '[SeasLog Test]', ['level' => 'DEBUG']); + $logger->log(Logger::WARNING, '[SeasLog Test]', ['level' => 'WARNING']); + $logger->log(Logger::ERROR, '[SeasLog Test]', ['level' => 'ERROR']); + $logger->log(Logger::INFO, '[SeasLog Test]', ['level' => 'INFO']); + $logger->log(Logger::CRITICAL, '[SeasLog Test]', ['level' => 'CRITICAL']); + $logger->log(Logger::EMERGENCY, '[SeasLog Test]', ['level' => 'EMERGENCY']); + $logger->log(Logger::NOTICE, '[SeasLog Test]', ['level' => 'NOTICE']); + $logger->log(Logger::ALERT, '[SeasLog Test]', ['level' => 'ALERT']); + + $logger->log(0, '[SeasLog Test]', ['level' => 'default']); + $logger->log(Logger::ALL - 1, '[SeasLog Test]', ['level' => 'default']); + + $this->assertTrue($logger::closeLoggerStream(SEASLOG_CLOSE_LOGGER_STREAM_MOD_ALL)); + } + + public function testCloseLoggerStream() + { + $logger = $this->init(1); + $logger->setBasePath('/tmp/PandaLogger'); + $logger->log(Logger::DEBUG, '[SeasLog Test]', ['level' => 'DEBUG']); + $logger->log(Logger::WARNING, '[SeasLog Test]', ['level' => 'WARNING']); + $logger->log(Logger::ERROR, '[SeasLog Test]', ['level' => 'ERROR']); + $logger->log(Logger::INFO, '[SeasLog Test]', ['level' => 'INFO']); + $logger->log(Logger::CRITICAL, '[SeasLog Test]', ['level' => 'CRITICAL']); + $logger->log(Logger::EMERGENCY, '[SeasLog Test]', ['level' => 'EMERGENCY']); + $logger->log(Logger::NOTICE, '[SeasLog Test]', ['level' => 'NOTICE']); + $logger->log(Logger::ALERT, '[SeasLog Test]', ['level' => 'ALERT']); + + $logger->log(0, '[SeasLog Test]', ['level' => 'default']); + $logger->log(Logger::ALL - 1, '[SeasLog Test]', ['level' => 'default']); + + $this->assertTrue($logger::closeLoggerStream(SEASLOG_CLOSE_LOGGER_STREAM_MOD_ASSIGN, '/tmp/PandaLogger')); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 2d75cc8..a185b95 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,7 +9,7 @@ * with this source code in the file LICENSE. */ -namespace SeasX\SeasLogger\Tests; +namespace Seasx\SeasLogger\Tests; use PHPUnit\Framework\TestCase as PHPUnitTestCase;