Merge branch 'master' into pr/827

This commit is contained in:
李铭昕 2019-11-02 14:24:52 +08:00
commit 8e32291fcd
89 changed files with 3937 additions and 87 deletions

View File

@ -38,6 +38,6 @@ before_script:
- composer config -g process-timeout 900 && composer update - composer config -g process-timeout 900 && composer update
script: script:
- composer analyse src/di src/json-rpc src/tracer - composer analyse src/di src/json-rpc src/tracer src/metric
- composer test -- --exclude-group NonCoroutine - composer test -- --exclude-group NonCoroutine
- vendor/bin/phpunit --group NonCoroutine - vendor/bin/phpunit --group NonCoroutine

View File

@ -1,24 +1,44 @@
# v1.1.4 - TBD # v1.1.5 - TBD
## Added
- [#832](https://github.com/hyperf/hyperf/pull/832) Added `Hyperf\Utils\Codec\Json`.
- [#833](https://github.com/hyperf/hyperf/pull/833) Added `Hyperf\Utils\Backoff`.
## Fixed
- [#835](https://github.com/hyperf/hyperf/pull/835) Fixed `Request::inputs` default value does not works.
## Optimized
- [#832](https://github.com/hyperf/hyperf/pull/832) Optimized that response will throw a exception when json format failed.
# v1.1.4 - 2019-10-31
## Added ## Added
- [#778](https://github.com/hyperf/hyperf/pull/778) Added `PUT` and `DELETE` for `Hyperf\Testing\Client`. - [#778](https://github.com/hyperf/hyperf/pull/778) Added `PUT` and `DELETE` for `Hyperf\Testing\Client`.
- [#784](https://github.com/hyperf/hyperf/pull/784) Add Metric Component
- [#795](https://github.com/hyperf/hyperf/pull/795) Added `restartInterval` for `AbstractProcess`. - [#795](https://github.com/hyperf/hyperf/pull/795) Added `restartInterval` for `AbstractProcess`.
- [#804](https://github.com/hyperf/hyperf/pull/804) Added `BeforeHandle` `AfterHandle` and `FailToHandle` for command.
## Fixed ## Fixed
- [#779](https://github.com/hyperf/hyperf/pull/779) Fixed bug that JPG file cannot be verified. - [#779](https://github.com/hyperf/hyperf/pull/779) Fixed bug that JPG file cannot be verified.
- [#787](https://github.com/hyperf/hyperf/pull/787) Fixed bug that "--class" option does not exist. - [#787](https://github.com/hyperf/hyperf/pull/787) Fixed bug that "--class" option does not exist.
- [#795](https://github.com/hyperf/hyperf/pull/795) Fixed process not restart when throw an exception. - [#795](https://github.com/hyperf/hyperf/pull/795) Fixed process not restart when throw an exception.
- [#796](https://github.com/hyperf/hyperf/pull/796) Fixed `config_etcd.enable` does not works.
## Optimized ## Optimized
- [#781](https://github.com/hyperf/hyperf/pull/781) Publish validation language package according to translation setting. - [#781](https://github.com/hyperf/hyperf/pull/781) Publish validation language package according to translation setting.
- [#796](https://github.com/hyperf/hyperf/pull/796) Don't remake HandlerStack for etcd.
- [#797](https://github.com/hyperf/hyperf/pull/797) Use channel to communicate, instead of sharing mem - [#797](https://github.com/hyperf/hyperf/pull/797) Use channel to communicate, instead of sharing mem
## Changed ## Changed
- [#793](https://github.com/hyperf/hyperf/pull/793) Changed `protected` to `public` for `Pool::getConnectionsInChannel`. - [#793](https://github.com/hyperf/hyperf/pull/793) Changed `protected` to `public` for `Pool::getConnectionsInChannel`.
- [#811](https://github.com/hyperf/hyperf/pull/811) Command `di:init-proxy` does not clear the runtime cache, If you want to delete them, use `vendor/bin/init-proxy.sh` instead.
# v1.1.3 - 2019-10-24 # v1.1.3 - 2019-10-24

View File

@ -12,11 +12,11 @@
"php": ">=7.2", "php": ">=7.2",
"ext-json": "*", "ext-json": "*",
"ext-swoole": ">=4.4", "ext-swoole": ">=4.4",
"egulias/email-validator": "^2.1",
"bandwidth-throttle/token-bucket": "^2.0", "bandwidth-throttle/token-bucket": "^2.0",
"doctrine/annotations": "^1.6", "doctrine/annotations": "^1.6",
"doctrine/inflector": "^1.3", "doctrine/inflector": "^1.3",
"doctrine/instantiator": "^1.0", "doctrine/instantiator": "^1.0",
"egulias/email-validator": "^2.1",
"elasticsearch/elasticsearch": "^6.1", "elasticsearch/elasticsearch": "^6.1",
"fig/http-message-util": "^1.1.2", "fig/http-message-util": "^1.1.2",
"google/protobuf": "^3.6.1", "google/protobuf": "^3.6.1",
@ -37,9 +37,11 @@
"psr/log": "^1.0", "psr/log": "^1.0",
"psr/simple-cache": "^1.0", "psr/simple-cache": "^1.0",
"squizlabs/php_codesniffer": "^3.4", "squizlabs/php_codesniffer": "^3.4",
"start-point/etcd-php": "^1.1",
"symfony/console": "^4.2", "symfony/console": "^4.2",
"symfony/finder": "^4.1", "symfony/finder": "^4.1",
"vlucas/phpdotenv": "^3.1" "vlucas/phpdotenv": "^3.1",
"endclothing/prometheus_client_php": "^0.9.1"
}, },
"require-dev": { "require-dev": {
"doctrine/common": "@stable", "doctrine/common": "@stable",
@ -52,7 +54,9 @@
"phpunit/phpunit": "^7.0.0", "phpunit/phpunit": "^7.0.0",
"swoft/swoole-ide-helper": "dev-master", "swoft/swoole-ide-helper": "dev-master",
"symfony/property-access": "^4.3", "symfony/property-access": "^4.3",
"symfony/serializer": "^4.3" "symfony/serializer": "^4.3",
"influxdb/influxdb-php": "^1.15.0",
"domnikl/statsd": "^3.0.1"
}, },
"replace": { "replace": {
"hyperf/amqp": "self.version", "hyperf/amqp": "self.version",
@ -85,6 +89,7 @@
"hyperf/http-server": "self.version", "hyperf/http-server": "self.version",
"hyperf/logger": "self.version", "hyperf/logger": "self.version",
"hyperf/memory": "self.version", "hyperf/memory": "self.version",
"hyperf/metric": "self.version",
"hyperf/model-cache": "self.version", "hyperf/model-cache": "self.version",
"hyperf/paginator": "self.version", "hyperf/paginator": "self.version",
"hyperf/pool": "self.version", "hyperf/pool": "self.version",
@ -147,6 +152,7 @@
"Hyperf\\LoadBalancer\\": "src/load-balancer/src/", "Hyperf\\LoadBalancer\\": "src/load-balancer/src/",
"Hyperf\\Logger\\": "src/logger/src/", "Hyperf\\Logger\\": "src/logger/src/",
"Hyperf\\Memory\\": "src/memory/src/", "Hyperf\\Memory\\": "src/memory/src/",
"Hyperf\\Metric\\": "src/metric/src/",
"Hyperf\\ModelCache\\": "src/model-cache/src/", "Hyperf\\ModelCache\\": "src/model-cache/src/",
"Hyperf\\ModelListener\\": "src/model-listener/src/", "Hyperf\\ModelListener\\": "src/model-listener/src/",
"Hyperf\\Paginator\\": "src/paginator/src/", "Hyperf\\Paginator\\": "src/paginator/src/",
@ -207,6 +213,7 @@
"HyperfTest\\JsonRpc\\": "src/json-rpc/tests/", "HyperfTest\\JsonRpc\\": "src/json-rpc/tests/",
"HyperfTest\\LoadBalancer\\": "src/load-balancer/tests/", "HyperfTest\\LoadBalancer\\": "src/load-balancer/tests/",
"HyperfTest\\Logger\\": "src/logger/tests/", "HyperfTest\\Logger\\": "src/logger/tests/",
"HyperfTest\\Metric\\": "src/metric/tests/",
"HyperfTest\\ModelCache\\": "src/model-cache/tests/", "HyperfTest\\ModelCache\\": "src/model-cache/tests/",
"HyperfTest\\ModelListener\\": "src/model-listener/tests/", "HyperfTest\\ModelListener\\": "src/model-listener/tests/",
"HyperfTest\\Paginator\\": "src/paginator/tests/", "HyperfTest\\Paginator\\": "src/paginator/tests/",
@ -261,6 +268,7 @@
"Hyperf\\LoadBalancer\\ConfigProvider", "Hyperf\\LoadBalancer\\ConfigProvider",
"Hyperf\\Logger\\ConfigProvider", "Hyperf\\Logger\\ConfigProvider",
"Hyperf\\Memory\\ConfigProvider", "Hyperf\\Memory\\ConfigProvider",
"Hyperf\\Metric\\ConfigProvider",
"Hyperf\\ModelCache\\ConfigProvider", "Hyperf\\ModelCache\\ConfigProvider",
"Hyperf\\ModelListener\\ConfigProvider", "Hyperf\\ModelListener\\ConfigProvider",
"Hyperf\\Paginator\\ConfigProvider", "Hyperf\\Paginator\\ConfigProvider",

View File

@ -30,17 +30,17 @@ runtime/container/proxy/
Re-genenrate command Re-genenrate command
``` ```
php bin/hyperf.php di:init-proxy vendor/bin/init-proxy.sh
``` ```
So the command to run unit test can use the following instead So the command to run unit test can use the following instead
``` ```
php bin/hyperf.php di:init-proxy && composer test vendor/bin/init-proxy.sh && composer test
``` ```
Similarly, the command to start the server can also use the following instead Similarly, the command to start the server can also use the following instead
``` ```
php bin/hyperf.php di:init-proxy && php bin/hyperf.php start vendor/bin/init-proxy.sh && php bin/hyperf.php start
``` ```
## Docker build failure ## Docker build failure

View File

@ -68,4 +68,4 @@ class FooAspect extends AbstractAspect
在部署生产环境时,我们可能会希望 Hyperf 提前将所有代理类提前生成,而不是使用时动态的生成,可以通过 `php bin/hyperf.php di:init-proxy` 命令来生成所有代理类,该命令会忽视现有的代理类缓存,全部重新生成。 在部署生产环境时,我们可能会希望 Hyperf 提前将所有代理类提前生成,而不是使用时动态的生成,可以通过 `php bin/hyperf.php di:init-proxy` 命令来生成所有代理类,该命令会忽视现有的代理类缓存,全部重新生成。
基于以上,我们可以将生成代理类的命令和启动服务的命令结合起来,`php bin/hyperf.php di:init-proxy && php bin/hyperf.php start` 来达到自动重新生成所有代理类缓存然后启动服务的目的。 基于以上,我们可以将生成代理类的命令和启动服务的命令结合起来,`vendor/bin/init-proxy.sh && php bin/hyperf.php start` 来达到自动重新生成所有代理类缓存然后启动服务的目的。

View File

@ -117,4 +117,5 @@
- [yurunsoft/pay-sdk](https://github.com/Yurunsoft/PaySDK) 支持 Swoole 协程的支付宝/微信支付SDK - [yurunsoft/pay-sdk](https://github.com/Yurunsoft/PaySDK) 支持 Swoole 协程的支付宝/微信支付SDK
- [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDKQQ、微信、微博、Github、Gitee等 - [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDKQQ、微信、微博、Github、Gitee等
- [overtrue/wechat](zh/sdks/wechat) EasyWeChat一个流行的微信 SDK非微信官方 SDK - [overtrue/wechat](zh/sdks/wechat) EasyWeChat一个流行的微信 SDK非微信官方 SDK
- [Yurunsoft/PHPMailer-Swoole](https://github.com/Yurunsoft/PHPMailer-Swoole) Swoole环境下的 PHPMailer

View File

@ -203,7 +203,7 @@ $result = parallel([
use Hyperf\Utils\Coroutine\Concurrent; use Hyperf\Utils\Coroutine\Concurrent;
$concurrent = new Concurrent(10, 1); $concurrent = new Concurrent(10);
for ($i = 0; $i < 15; ++$i) { for ($i = 0; $i < 15; ++$i) {
$concurrent->create(function () use ($count) { $concurrent->create(function () use ($count) {

View File

@ -260,7 +260,7 @@ class UserServiceFactory
} }
``` ```
`UserService`在构造函数提供一个参数接收对应的值: `UserService`可以在构造函数提供一个参数接收对应的值:
```php ```php
<?php <?php

310
doc/zh/metric.md Normal file
View File

@ -0,0 +1,310 @@
# 服务监控
微服务治理的一个核心需求便是服务可观察性。作为微服务的牧羊人,要做到时刻掌握各项服务的健康状态,并非易事。云原生时代这一领域内涌现出了诸多解决方案。本组件对可观察性当中的重要支柱遥测与监控进行了抽象,方便使用者与既有基础设施快速结合,同时避免供应商锁定。
## 安装
### 通过 Composer 安装组件
```bash
composer require hyperf/metric
```
[hyperf/metric](https://github.com/hyperf/metric) 组件默认安装了 [Prometheus](https://prometheus.io/) 相关依赖。如果要使用 [StatsD](https://github.com/statsd/statsd) 或 [InfluxDB](http://influxdb.com),还需要执行下面的命令安装对应的依赖:
```bash
# StatsD 所需依赖
composer require domnikl/statsd
# InfluxDB 所需依赖
composer require influxdb/influxdb-php
```
### 增加组件配置
如文件不存在,可执行下面的命令增加 `config/autoload/metric.php` 配置文件:
```bash
php bin/hyperf.php vendor:publish hyperf/metric
```
## 使用
### 配置
#### 选项
* `default`:配置文件内的 `default` 对应的值则为使用的驱动名称。驱动的具体配置在 `metric` 项下定义,使用与 `key` 相同的驱动。
```php
'default' => env('TELEMETRY_DRIVER', 'prometheus'),
```
* `use_standalone_process`: 是否使用 `独立监控进程`。推荐开启。关闭后将在 `Worker进程` 中处理指标收集与上报。
```php
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
```
* `enable_default_metric`: 是否统计默认指标。默认指标包括内存占用、系统 CPU 负载以及Swoole Server 指标和 Swoole Coroutine 指标。
```php
'enable_default_metric' => env('TELEMETRY_ENABLE_DEFAULT_TELEMETRY', true),
```
* `default_metric_interval`: 默认指标推送周期,单位为秒,下同。
```php
'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
```
#### 配置 Prometheus
使用 Prometheus 时,在配置文件中的 `metric` 项增加 Prometheus 的具体配置。
```php
use Hyperf\Metric\Adapter\Prometheus\Constants;
return [
'default' => env('TELEMETRY_DRIVER', 'prometheus'),
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
'enable_default_metric' => env('TELEMETRY_ENABLE_DEFAULT_TELEMETRY', true),
'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
'metric' => [
'prometheus' => [
'driver' => Hyperf\Metric\Adapter\Prometheus\MetricFactory::class,
'mode' => Constants::SCRAPE_MODE,
'namespace' => env('APP_NAME', 'skeleton'),
'scrape_host' => env('PROMETHEUS_SCRAPE_HOST', '0.0.0.0'),
'scrape_port' => env('PROMETHEUS_SCRAPE_PORT', '9502'),
'scrape_path' => env('PROMETHEUS_SCRAPE_PATH', '/metrics'),
'push_host' => env('PROMETHEUS_PUSH_HOST', '0.0.0.0'),
'push_port' => env('PROMETHEUS_PUSH_PORT', '9091'),
'push_interval' => env('PROMETHEUS_PUSH_INTERVAL', 5),
],
],
];
```
Prometheus 有两种工作模式,爬模式与推模式(通过 Prometheus Pushgateway ),本组件均可支持。
使用爬模式Prometheus 官方推荐)时需设置:
```php
'mode' => Constants::SCRAPE_MODE
```
并配置爬取地址 `scrape_host`、爬取端口 `scrape_port`、爬取路径 `scrape_path`。Prometheus 可以在对应配置下以HTTP访问形式拉取全部指标。
> 注意:爬模式下,必须启用独立进程,即 use_standalone_process = true。
使用推模式时需设置:
```php
'mode' => Constants::PUSH_MODE
```
并配置推送地址 `push_host`、推送端口 `push_port`、推送间隔 `push_interval`。只建议离线任务使用推模式。
#### 配置 StatsD
使用 StatsD 时,在配置文件中的 `metric` 项增加 StatsD 的具体配置。
```php
return [
'default' => env('TELEMETRY_DRIVER', 'statd'),
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
'enable_default_metric' => env('TELEMETRY_ENABLE_DEFAULT_TELEMETRY', true),
'metric' => [
'statsd' => [
'driver' => Hyperf\Metric\Adapter\StatsD\MetricFactory::class,
'namespace' => env('APP_NAME', 'skeleton'),
'udp_host' => env('STATSD_UDP_HOST', '127.0.0.1'),
'udp_port' => env('STATSD_UDP_PORT', '8125'),
'enable_batch' => env('STATSD_ENABLE_BATCH', true),
'push_interval' => env('STATSD_PUSH_INTERVAL', 5),
'sample_rate' => env('STATSD_SAMPLE_RATE', 1.0),
],
],
];
```
StatsD 目前只支持 UDP 模式,需要配置 UDP 地址 `udp_host`UDP 端口 `udp_port`、是否批量推送 `enable_batch`(减少请求次数)、批量推送间隔 `push_interval` 以及采样率`sample_rate`。
#### 配置 InfluxDB
使用 InfluxDB 时,在配置文件中的 `metric` 项增加 InfluxDB 的具体配置。
```php
return [
'default' => env('TELEMETRY_DRIVER', 'influxdb'),
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
'enable_default_metric' => env('TELEMETRY_ENABLE_DEFAULT_TELEMETRY', true),
'metric' => [
'influxdb' => [
'driver' => Hyperf\Metric\Adapter\InfluxDB\MetricFactory::class,
'namespace' => env('APP_NAME', 'skeleton'),
'host' => env('INFLUXDB_HOST', '127.0.0.1'),
'port' => env('INFLUXDB_PORT', '8086'),
'username' => env('INFLUXDB_USERNAME', ''),
'password' => env('INFLUXDB_PASSWORD', ''),
'dbname' => env('INFLUXDB_DBNAME', true),
'push_interval' => env('INFLUXDB_PUSH_INTERVAL', 5),
],
],
];
```
InfluxDB 使用默认的 HTTP 模式,需要配置地址 `host`UDP端口 `port`、用户名 `username`、密码 `password`、`dbname` 数据表以及批量推送间隔 `push_interval`
### 基本抽象
遥测组件对常用的三种数据类型进行了抽象,以确保解耦具体实现。
三种类型分别为:
* 计数器(Counter): 用于描述单向递增的某种指标。如 HTTP 请求计数。
```php
interface CounterInterface
{
public function with(string ...$labelValues): self;
public function add(int $delta);
}
```
* 测量器(Gauge):用于描述某种随时间发生增减变化的指标。如连接池内的可用连接数。
```php
interface GaugeInterface
{
public function with(string ...$labelValues): self;
public function set(float $value);
public function add(float $delta);
}
```
* 直方图(Histogram)用于描述对某一事件的持续观测后产生的统计学分布通常表示为百分位数或分桶。如HTTP请求延迟。
```php
interface HistogramInterface
{
public function with(string ...$labelValues): self;
public function put(float $sample);
}
```
### 配置中间件
配置完驱动之后,只需配置一下中间件就能启用请求 Histogram 统计功能。
打开 `config/autoload/middlewares.php` 文件,示例为在 `http` Server 中启用中间件。
```php
<?php
declare(strict_types=1);
return [
'http' => [
\Hyperf\Metric\Middleware\MetricMiddeware::class,
],
];
```
### 自定义使用
通过HTTP中间件遥测仅仅是本组件用途的冰山一角您可以注入 `Hyperf\Metric\Contract\MetricFactoryInterface` 类来自行遥测业务数据。比如:创建的订单数量、广告的点击数量等。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Model\Order;
use Hyperf\Metric\Contract\MetricFactoryInterface;
class IndexController extends AbstractController
{
/**
* @Inject
* @var MetricFactoryInterface
*/
private $metricFactory;
public function create(Order $order)
{
$counter = $this->metricFactory->makeCounter('order_created', ['order_type']);
$counter->with($order->type)->add(1);
// 订单逻辑...
}
}
```
`MetricFactoryInterface` 中包含如下工厂方法来生成对应的三种基本统计类型。
```php
public function makeCounter($name, $labelNames): CounterInterface;
public function makeGauge($name, $labelNames): GaugeInterface;
public function makeHistogram($name, $labelNames): HistogramInterface;
```
上述例子是统计请求范围内的产生的指标。有时候我们需要统计的指标是面向完整生命周期的,比如统计异步队列长度或库存商品数量。此种场景下可以监听 `MetricFactoryReady` 事件。
```php
<?php
declare(strict_types=1);
namespace App\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Metric\Event\MetricFactoryReady;
use Psr\Container\ContainerInterface;
use Redis;
class OnMetricFactoryReady implements ListenerInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function listen(): array
{
return [
MetricFactoryReady::class,
];
}
public function process(object $event)
{
$redis = $this->container->get(Redis::class);
$gauge = $event
->factory
->makeGauge('queue_length', ['driver'])
->with('redis');
while (true) {
$length = $redis->llen('queue');
$gauge->set($length);
sleep(1);
}
}
}
```
> 工程上讲,直接从 Redis 查询队列长度不太合适,应该通过队列驱动 `DriverInterface` 接口下的 `info()` 方法来获取队列长度。这里只做简易演示。您可以在本组件源码的`src/Listener` 文件夹下找到完整例子。
### 注解
您可以使用 `@Counter(name="stat_name_here")``@Histogram(name="stat_name_here")` 来统计切面的调用次数和运行时间。
关于注解的使用请参阅[注解章节](https://doc.hyperf.io/#/zh/annotation)。

View File

@ -24,28 +24,33 @@ swoole.use_shortname = 'Off'
代理类缓存一旦生成,将不会再重新覆盖。所以当你修改了已经生成代理类的文件时,需要手动清理。 代理类缓存一旦生成,将不会再重新覆盖。所以当你修改了已经生成代理类的文件时,需要手动清理。
代理类位置如下 代理类位置如下
``` ```
runtime/container/proxy/ runtime/container/proxy/
``` ```
重新生成缓存命令,新缓存会覆盖原目录 重新生成缓存命令,新缓存会覆盖原目录
```bash ```bash
php bin/hyperf.php di:init-proxy vendor/bin/init-proxy.sh
``` ```
删除代理类缓存 删除代理类缓存
```bash ```bash
rm -rf ./runtime/container/proxy rm -rf ./runtime/container/proxy
``` ```
所以单测命令可以使用以下代替: 所以单测命令可以使用以下代替:
```bash ```bash
php bin/hyperf.php di:init-proxy && composer test vendor/bin/init-proxy.sh && composer test
``` ```
同理,启动命令可以使用以下代替 同理,启动命令可以使用以下代替
```bash ```bash
php bin/hyperf.php di:init-proxy && php bin/hyperf.php start vendor/bin/init-proxy.sh && php bin/hyperf.php start
``` ```
## PHP7.3下预先生成代理的脚本 执行失败 ## PHP7.3下预先生成代理的脚本 执行失败

View File

@ -2,6 +2,8 @@
在 Hyperf 里可通过 `Hyperf\HttpServer\Contract\ResponseInterface` 接口类来注入 `Response` 代理对象对响应进行处理,默认返回 `Hyperf\HttpServer\Response` 对象,该对象可直接调用所有 `Psr\Http\Message\ResponseInterface` 的方法。 在 Hyperf 里可通过 `Hyperf\HttpServer\Contract\ResponseInterface` 接口类来注入 `Response` 代理对象对响应进行处理,默认返回 `Hyperf\HttpServer\Response` 对象,该对象可直接调用所有 `Psr\Http\Message\ResponseInterface` 的方法。
> 注意 PSR-7 标准为 响应(Response) 进行了 immutable 机制 的设计,所有以 with 开头的方法的返回值都是一个新对象,不会修改原对象的值
## 返回 Json 格式 ## 返回 Json 格式
`Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `json($data)` 方法用于快速返回 `Json` 格式,并设置 `Content-Type``application/json``$data` 接受一个数组或为一个实现了 `Hyperf\Utils\Contracts\Arrayable` 接口的对象。 `Hyperf\HttpServer\Contract\ResponseInterface` 提供了 `json($data)` 方法用于快速返回 `Json` 格式,并设置 `Content-Type``application/json``$data` 接受一个数组或为一个实现了 `Hyperf\Utils\Contracts\Arrayable` 接口的对象。

View File

@ -60,6 +60,7 @@
* [服务限流](zh/rate-limit.md) * [服务限流](zh/rate-limit.md)
* [配置中心](zh/config-center.md) * [配置中心](zh/config-center.md)
* [调用链追踪](zh/tracer.md) * [调用链追踪](zh/tracer.md)
* [服务监控](zh/metric.md)
* 其它组件 * 其它组件

View File

@ -37,7 +37,7 @@ require BASE_PATH . '/config/container.php';
``` ```
# 重新生成代理类 # 重新生成代理类
php bin/hyperf.php di:init-proxy vendor/bin/init-proxy.sh
# 运行单元测试 # 运行单元测试
composer test composer test
``` ```

View File

@ -21,6 +21,7 @@
<directory suffix="Test.php">./src/di/tests</directory> <directory suffix="Test.php">./src/di/tests</directory>
<directory suffix="Test.php">./src/dispatcher/tests</directory> <directory suffix="Test.php">./src/dispatcher/tests</directory>
<directory suffix="Test.php">./src/elasticsearch/tests</directory> <directory suffix="Test.php">./src/elasticsearch/tests</directory>
<directory suffix="Test.php">./src/etcd/tests</directory>
<directory suffix="Test.php">./src/event/tests</directory> <directory suffix="Test.php">./src/event/tests</directory>
<directory suffix="Test.php">./src/exception-handler/tests</directory> <directory suffix="Test.php">./src/exception-handler/tests</directory>
<directory suffix="Test.php">./src/grpc-client/tests</directory> <directory suffix="Test.php">./src/grpc-client/tests</directory>
@ -30,6 +31,7 @@
<directory suffix="Test.php">./src/http-server/tests</directory> <directory suffix="Test.php">./src/http-server/tests</directory>
<directory suffix="Test.php">./src/json-rpc/tests</directory> <directory suffix="Test.php">./src/json-rpc/tests</directory>
<directory suffix="Test.php">./src/logger/tests</directory> <directory suffix="Test.php">./src/logger/tests</directory>
<directory suffix="Test.php">./src/metric/tests</directory>
<directory suffix="Test.php">./src/model-cache/tests</directory> <directory suffix="Test.php">./src/model-cache/tests</directory>
<directory suffix="Test.php">./src/paginator/tests</directory> <directory suffix="Test.php">./src/paginator/tests</directory>
<directory suffix="Test.php">./src/pool/tests</directory> <directory suffix="Test.php">./src/pool/tests</directory>

View File

@ -20,8 +20,9 @@
}, },
"require": { "require": {
"php": ">=7.2", "php": ">=7.2",
"symfony/console": "^4.2", "hyperf/utils": "~1.1.0",
"hyperf/utils": "~1.1.0" "psr/event-dispatcher": "^1.0",
"symfony/console": "^4.2"
}, },
"require-dev": { "require-dev": {
"malukenho/docheader": "^0.1.6", "malukenho/docheader": "^0.1.6",
@ -32,6 +33,9 @@
"suggest": { "suggest": {
"hyperf/di": "Required to use annotations." "hyperf/di": "Required to use annotations."
}, },
"config": {
"sort-packages": true
},
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.1-dev" "dev-master": "1.1-dev"

View File

@ -15,6 +15,7 @@ namespace Hyperf\Command;
use Hyperf\Utils\Contracts\Arrayable; use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Coroutine; use Hyperf\Utils\Coroutine;
use Hyperf\Utils\Str; use Hyperf\Utils\Str;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
@ -58,6 +59,11 @@ abstract class Command extends SymfonyCommand
*/ */
protected $coroutine = true; protected $coroutine = true;
/**
* @var null|EventDispatcherInterface
*/
protected $eventDispatcher;
/** /**
* @var int * @var int
*/ */
@ -381,13 +387,25 @@ abstract class Command extends SymfonyCommand
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
if ($this->coroutine && ! Coroutine::inCoroutine()) { $callback = function () {
run(function () { try {
$this->eventDispatcher && $this->eventDispatcher->dispatch(new Event\BeforeHandle($this));
call([$this, 'handle']); call([$this, 'handle']);
}, $this->hookFlags); $this->eventDispatcher && $this->eventDispatcher->dispatch(new Event\AfterHandle($this));
} catch (\Throwable $exception) {
if (! $this->eventDispatcher) {
throw $exception;
}
$this->eventDispatcher->dispatch(new Event\FailToHandle($this, $exception));
}
};
if ($this->coroutine && ! Coroutine::inCoroutine()) {
run($callback, $this->hookFlags);
return 0; return 0;
} }
return call([$this, 'handle']);
return $callback();
} }
} }

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Command\Event;
class AfterHandle extends Event
{
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Command\Event;
class BeforeHandle extends Event
{
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Command\Event;
use Hyperf\Command\Command;
abstract class Event
{
/**
* @var Command
*/
protected $command;
public function __construct(Command $command)
{
$this->command = $command;
}
public function getCommand(): Command
{
return $this->command;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Command\Event;
use Hyperf\Command\Command;
use Throwable;
class FailToHandle extends Event
{
/**
* @var Throwable
*/
protected $throwable;
public function __construct(Command $command, Throwable $throwable)
{
parent::__construct($command);
$this->throwable = $throwable;
}
public function getThrowable(): Throwable
{
return $this->throwable;
}
}

View File

@ -74,6 +74,10 @@ class BootProcessListener implements ListenerInterface
public function process(object $event) public function process(object $event)
{ {
if (! $this->config->get('config_etcd.enable', false)) {
return;
}
if ($config = $this->client->pull()) { if ($config = $this->client->pull()) {
$configurations = $this->format($config); $configurations = $this->format($config);
foreach ($configurations as $kv) { foreach ($configurations as $kv) {

View File

@ -75,6 +75,10 @@ class OnPipeMessageListener implements ListenerInterface
*/ */
public function process(object $event) public function process(object $event)
{ {
if (! $this->config->get('config_etcd.enable', false)) {
return;
}
if (property_exists($event, 'data') && $event->data instanceof PipeMessage) { if (property_exists($event, 'data') && $event->data instanceof PipeMessage) {
/** @var PipeMessage $data */ /** @var PipeMessage $data */
$data = $event->data; $data = $event->data;

19
src/di/bin/init-proxy.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
basepath=$(cd `dirname $0`; pwd)
echo ../../
if [ ! -f "composer.lock" ]; then
echo "Not found composer.lock, please composer install first."
exit
fi
rm -rf runtime
echo "Runtime cleared"
php bin/hyperf.php di:init-proxy
echo "Finish!"

View File

@ -56,6 +56,7 @@
} }
}, },
"bin": [ "bin": [
"bin/init-proxy.sh"
], ],
"scripts": { "scripts": {
"cs-fix": "php-cs-fixer fix $1", "cs-fix": "php-cs-fixer fix $1",

View File

@ -18,8 +18,6 @@ use Hyperf\Di\Annotation\Scanner;
use Hyperf\Di\Container; use Hyperf\Di\Container;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class InitProxyCommand extends Command class InitProxyCommand extends Command
{ {
@ -49,23 +47,13 @@ class InitProxyCommand extends Command
public function handle() public function handle()
{ {
$this->warn('This command does not clear the runtime cache, If you want to delete them, use `vendor/bin/init-proxy.sh` instead.');
$this->createAopProxies(); $this->createAopProxies();
$this->output->writeln('<info>Proxy class create success.</info>'); $this->output->writeln('<info>Proxy class create success.</info>');
} }
protected function clearRuntime($paths)
{
$finder = new Finder();
$finder->files()->in($paths)->name(['*.php', '*.cache']);
/** @var SplFileInfo $file */
foreach ($finder as $file) {
$path = $file->getRealPath();
@unlink($path);
}
}
protected function getScanDir() protected function getScanDir()
{ {
if (! defined('BASE_PATH')) { if (! defined('BASE_PATH')) {
@ -91,11 +79,6 @@ class InitProxyCommand extends Command
{ {
$scanDirs = $this->getScanDir(); $scanDirs = $this->getScanDir();
$runtime = BASE_PATH . '/runtime/container/';
if (is_dir($runtime)) {
$this->clearRuntime($runtime);
}
$meta = $this->scanner->scan($scanDirs); $meta = $this->scanner->scan($scanDirs);
$classCollection = array_keys($meta); $classCollection = array_keys($meta);

View File

@ -20,7 +20,9 @@
}, },
"require": { "require": {
"php": ">=7.2", "php": ">=7.2",
"hyperf/utils": "~1.1.0" "hyperf/guzzle": "~1.1.0",
"hyperf/utils": "~1.1.0",
"start-point/etcd-php": "^1.1"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^2.14", "friendsofphp/php-cs-fixer": "^2.14",
@ -28,8 +30,6 @@
"swoft/swoole-ide-helper": "dev-master" "swoft/swoole-ide-helper": "dev-master"
}, },
"suggest": { "suggest": {
"start-point/etcd-php": "Etcd v3 http client.",
"linkorb/etcd-php": "Etcd v2 http client."
}, },
"config": { "config": {
"sort-packages": true "sort-packages": true

View File

@ -14,8 +14,7 @@ namespace Hyperf\Etcd;
use GuzzleHttp\HandlerStack; use GuzzleHttp\HandlerStack;
use Hyperf\Contract\ConfigInterface; use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\PoolHandler; use Hyperf\Guzzle\HandlerStackFactory;
use Hyperf\Guzzle\RetryMiddleware;
use Hyperf\Utils\Coroutine; use Hyperf\Utils\Coroutine;
abstract class Client abstract class Client
@ -35,37 +34,33 @@ abstract class Client
*/ */
protected $options = []; protected $options = [];
protected $client; /**
* @var HandlerStack[]
*/
protected $stacks;
public function __construct(ConfigInterface $config) /**
* @var HandlerStackFactory
*/
protected $factory;
public function __construct(ConfigInterface $config, HandlerStackFactory $factory)
{ {
$uri = $config->get('etcd.uri', 'http://127.0.0.1:2379'); $uri = $config->get('etcd.uri', 'http://127.0.0.1:2379');
$version = $config->get('etcd.version', 'v3beta'); $version = $config->get('etcd.version', 'v3beta');
$this->options = $config->get('etcd.options', []); $this->options = $config->get('etcd.options', []);
$this->baseUri = sprintf('%s/%s/', $uri, $version); $this->baseUri = sprintf('%s/%s/', $uri, $version);
$this->factory = $factory;
} }
protected function getDefaultHandler() protected function getDefaultHandler()
{ {
$handler = null; $id = (int) Coroutine::inCoroutine();
if (Coroutine::inCoroutine()) { if ($this->stacks[$id] instanceof HandlerStack) {
$handler = make(PoolHandler::class, [ return $this->stacks[$id];
'option' => [
'max_connections' => 50,
],
]);
} }
// Retry Middleware return $this->stacks[$id] = $this->factory->create();
$retry = make(RetryMiddleware::class, [
'retries' => 1,
'delay' => 10,
]);
$stack = HandlerStack::create($handler);
$stack->push($retry->getMiddleware(), 'retry');
return $stack;
} }
} }

View File

@ -14,6 +14,7 @@ namespace Hyperf\Etcd;
use Hyperf\Contract\ConfigInterface; use Hyperf\Contract\ConfigInterface;
use Hyperf\Etcd\Exception\ClientNotFindException; use Hyperf\Etcd\Exception\ClientNotFindException;
use Hyperf\Guzzle\HandlerStackFactory;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
class KVFactory class KVFactory
@ -23,7 +24,7 @@ class KVFactory
$config = $container->get(ConfigInterface::class); $config = $container->get(ConfigInterface::class);
$version = $config->get('etcd.version'); $version = $config->get('etcd.version');
$params = ['config' => $config]; $params = ['config' => $config, 'factory' => $container->get(HandlerStackFactory::class)];
switch ($version) { switch ($version) {
case 'v3': case 'v3':

View File

@ -12,13 +12,25 @@ declare(strict_types=1);
namespace HyperfTest\Etcd; namespace HyperfTest\Etcd;
use GuzzleHttp\Client;
use Hyperf\Config\Config; use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface; use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Container;
use Hyperf\Etcd\KVFactory; use Hyperf\Etcd\KVFactory;
use Hyperf\Etcd\KVInterface; use Hyperf\Etcd\KVInterface;
use Hyperf\Etcd\V3\EtcdClient;
use Hyperf\Etcd\V3\KV;
use Hyperf\Guzzle\HandlerStackFactory;
use Hyperf\Guzzle\PoolHandler;
use Hyperf\Pool\Channel;
use Hyperf\Pool\PoolOption;
use Hyperf\Pool\SimplePool\Connection;
use Hyperf\Pool\SimplePool\Pool;
use Hyperf\Pool\SimplePool\PoolFactory;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\Etcd\Stub\GuzzleClientStub;
use Mockery; use Mockery;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
/** /**
* @internal * @internal
@ -32,6 +44,29 @@ class KVTest extends TestCase
} }
public function testGetKyFromFactory() public function testGetKyFromFactory()
{
$kv = $this->getKVClient();
$this->assertInstanceOf(KVInterface::class, $kv);
}
public function testPutAndGet()
{
$kv = $this->getKVClient();
$res = $kv->put('/test/test2', 'Hello World!');
$this->assertArrayHasKey('header', $res);
$res = $kv->get('/test/test2');
$this->assertSame('Hello World!', $res['kvs'][0]['value']);
}
/**
* @return KVInterface
*/
protected function getKVClient()
{ {
$config = new Config([ $config = new Config([
'etcd' => [ 'etcd' => [
@ -43,12 +78,37 @@ class KVTest extends TestCase
], ],
]); ]);
$container = Mockery::mock(ContainerInterface::class); $container = Mockery::mock(Container::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('make')->with(EtcdClient::class, Mockery::any())->andReturnUsing(function ($class, $args) {
return new EtcdClient($args['client']);
});
$container->shouldReceive('make')->with(Client::class, Mockery::any())->andReturnUsing(function ($class, $args) {
return new GuzzleClientStub($args['config']);
});
$container->shouldReceive('make')->with(PoolHandler::class, Mockery::any())->andReturnUsing(function ($class, $args) use ($container) {
return new PoolHandler(new PoolFactory($container), $args['option']);
});
$container->shouldReceive('make')->with(Channel::class, Mockery::any())->andReturnUsing(function ($class, $args) {
return new Channel($args['size']);
});
$container->shouldReceive('make')->with(Connection::class, Mockery::any())->andReturnUsing(function ($class, $args) use ($container) {
return new Connection($container, $args['pool'], $args['callback']);
});
$container->shouldReceive('make')->with(PoolOption::class, Mockery::any())->andReturnUsing(function ($class, $args) {
return new PoolOption(...array_values($args));
});
$container->shouldReceive('make')->with(Pool::class, Mockery::any())->andReturnUsing(function ($class, $args) use ($container) {
return new Pool($container, $args['callback'], $args['option']);
});
$container->shouldReceive('make')->with(KV::class, Mockery::any())->andReturnUsing(function ($class, $args) {
return new KV(...array_values($args));
});
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config); $container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(HandlerStackFactory::class)->andReturn(new HandlerStackFactory());
$factory = new KVFactory(); $factory = new KVFactory();
$kv = $factory($container); return $factory($container);
$this->assertInstanceOf(KVInterface::class, $kv);
} }
} }

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Etcd\Stub;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;
class GuzzleClientStub extends Client
{
public function request($method, $uri = '', array $options = [])
{
if ($uri == 'kv/put') {
$stream = fopen('php://temp', 'r+');
fwrite($stream, '{"header":{"cluster_id":"11588568905070377092","member_id":"128088275939295631","revision":"10","raft_term":"3"}}');
fseek($stream, 0);
return new Response(200, [], new Stream($stream));
}
if ($uri == 'kv/range') {
$stream = fopen('php://temp', 'r+');
fwrite($stream, '{"header":{"cluster_id":"11588568905070377092","member_id":"128088275939295631","revision":"12","raft_term":"3"},"kvs":[{"key":"L3Rlc3QvdGVzdDI=","create_revision":"7","mod_revision":"12","version":"6","value":"SGVsbG8gV29ybGQh"}],"count":"1"}');
fseek($stream, 0);
return new Response(200, [], new Stream($stream));
}
return parent::request($method, $uri, $options);
}
}

View File

@ -93,10 +93,9 @@ class Request implements RequestInterface
public function inputs(array $keys, $default = null): array public function inputs(array $keys, $default = null): array
{ {
$data = $this->getInputData(); $data = $this->getInputData();
$result = $default ?? [];
foreach ($keys as $key) { foreach ($keys as $key) {
$result[$key] = data_get($data, $key); $result[$key] = data_get($data, $key, $default[$key] ?? null);
} }
return $result; return $result;

View File

@ -23,6 +23,7 @@ use Hyperf\HttpServer\Exception\Http\FileException;
use Hyperf\HttpServer\Exception\Http\InvalidResponseException; use Hyperf\HttpServer\Exception\Http\InvalidResponseException;
use Hyperf\Utils\ApplicationContext; use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\ClearStatCache; use Hyperf\Utils\ClearStatCache;
use Hyperf\Utils\Codec\Json;
use Hyperf\Utils\Context; use Hyperf\Utils\Context;
use Hyperf\Utils\Contracts\Arrayable; use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable; use Hyperf\Utils\Contracts\Jsonable;
@ -459,19 +460,13 @@ class Response implements PsrResponseInterface, ResponseInterface, Sendable
*/ */
protected function toJson($data): string protected function toJson($data): string
{ {
if (is_array($data)) { try {
return json_encode($data, JSON_UNESCAPED_UNICODE); $result = Json::encode($data);
} catch (\Throwable $exception) {
throw new EncodingException($exception->getMessage(), $exception->getCode());
} }
if ($data instanceof Jsonable) { return $result;
return (string) $data;
}
if ($data instanceof Arrayable) {
return json_encode($data->toArray(), JSON_UNESCAPED_UNICODE);
}
throw new EncodingException('Error encoding response data to JSON.');
} }
/** /**

View File

@ -29,6 +29,7 @@ class RequestTest extends TestCase
{ {
Mockery::close(); Mockery::close();
Context::set(ServerRequestInterface::class, null); Context::set(ServerRequestInterface::class, null);
Context::set('http.request.parsedData', null);
} }
public function testRequestHasFile() public function testRequestHasFile()
@ -60,4 +61,38 @@ class RequestTest extends TestCase
$res = $psrRequest->header('Hyperf-Version', 'v0'); $res = $psrRequest->header('Hyperf-Version', 'v0');
$this->assertSame('v1.0', $res); $this->assertSame('v1.0', $res);
} }
public function testRequestInput()
{
$psrRequest = Mockery::mock(ServerRequestInterface::class);
$psrRequest->shouldReceive('getParsedBody')->andReturn(['id' => 1]);
$psrRequest->shouldReceive('getQueryParams')->andReturn([]);
Context::set(ServerRequestInterface::class, $psrRequest);
$psrRequest = new Request();
$this->assertSame(1, $psrRequest->input('id'));
$this->assertSame('Hyperf', $psrRequest->input('name', 'Hyperf'));
}
public function testRequestAll()
{
$psrRequest = Mockery::mock(ServerRequestInterface::class);
$psrRequest->shouldReceive('getParsedBody')->andReturn(['id' => 1]);
$psrRequest->shouldReceive('getQueryParams')->andReturn(['name' => 'Hyperf']);
Context::set(ServerRequestInterface::class, $psrRequest);
$psrRequest = new Request();
$this->assertSame(['id' => 1, 'name' => 'Hyperf'], $psrRequest->all());
}
public function testRequestInputs()
{
$psrRequest = Mockery::mock(ServerRequestInterface::class);
$psrRequest->shouldReceive('getParsedBody')->andReturn(['id' => 1]);
$psrRequest->shouldReceive('getQueryParams')->andReturn([]);
Context::set(ServerRequestInterface::class, $psrRequest);
$psrRequest = new Request();
$this->assertSame(['id' => 1, 'name' => 'Hyperf'], $psrRequest->inputs(['id', 'name'], ['name' => 'Hyperf']));
}
} }

View File

@ -179,6 +179,20 @@ class ResponseTest extends TestCase
$this->assertSame('{"kstring":"string","kint1":1,"kint0":0,"kfloat":0.12345,"kfalse":false,"ktrue":true,"karray":{"kstring":"string","kint1":1,"kint0":0,"kfloat":0.12345,"kfalse":false,"ktrue":true}}', $json->getBody()->getContents()); $this->assertSame('{"kstring":"string","kint1":1,"kint0":0,"kfloat":0.12345,"kfalse":false,"ktrue":true,"karray":{"kstring":"string","kint1":1,"kint0":0,"kfloat":0.12345,"kfalse":false,"ktrue":true}}', $json->getBody()->getContents());
} }
public function testObjectToJson()
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
$psrResponse = new \Hyperf\HttpMessage\Base\Response();
Context::set(PsrResponseInterface::class, $psrResponse);
$response = new Response();
$json = $response->json((object) ['id' => 1, 'name' => 'Hyperf']);
$this->assertSame('{"id":1,"name":"Hyperf"}', $json->getBody()->getContents());
}
public function testPsrResponse() public function testPsrResponse()
{ {
$container = Mockery::mock(ContainerInterface::class); $container = Mockery::mock(ContainerInterface::class);

1
src/metric/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
/tests export-ignore

4
src/metric/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor/
composer.lock
*.cache
*.log

21
src/metric/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
src/metric/composer.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "hyperf/metric",
"license": "MIT",
"keywords": [
"php",
"hyperf",
"prometheus",
"statsd",
"metrics",
"influxdb"
],
"description": "hyperf metric component",
"require": {
"php": ">=7.2",
"psr/http-message": "^1.0",
"endclothing/prometheus_client_php": "^0.9.1",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"hyperf/contract": "~1.1.0",
"hyperf/utils": "~1.1.0",
"hyperf/guzzle": "~1.1.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
"friendsofphp/php-cs-fixer": "^2.9",
"influxdb/influxdb-php": "^1.15.0",
"domnikl/statsd": "^3.0.1"
},
"suggest": {
"domnikl/statsd": "Required to use StatdD driver.",
"influxdb/influxdb-php": "Required to use InfluxDB driver.",
"hyperf/di": "Required to use annotations.",
"hyperf/event": "Required to use listeners for default metrics.",
"hyperf/process": "Required to use standalone process, or you have to roll your own"
},
"autoload": {
"psr-4": {
"Hyperf\\Metric\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\Metric\\": "tests/"
}
},
"config": {
"sort-packages": true
},
"bin": [],
"scripts": {
"cs-fix": "php-cs-fixer fix $1",
"test": "phpunit --colors=always"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"hyperf": {
"config": "Hyperf\\Metric\\ConfigProvider"
}
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\Metric\Adapter\Prometheus\Constants;
return [
'default' => env('METRIC_DRIVER', 'prometheus'),
'use_standalone_process' => env('METRIC_USE_STANDALONE_PROCESS', true),
'enable_default_metric' => env('METRIC_ENABLE_DEFAULT_METRIC', true),
'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
'metric' => [
'prometheus' => [
'driver' => Hyperf\Metric\Adapter\Prometheus\MetricFactory::class,
'mode' => Constants::SCRAPE_MODE,
'namespace' => env('APP_NAME', 'skeleton'),
'scrape_host' => env('PROMETHEUS_SCRAPE_HOST', '0.0.0.0'),
'scrape_port' => env('PROMETHEUS_SCRAPE_PORT', '9502'),
'scrape_path' => env('PROMETHEUS_SCRAPE_PATH', '/metrics'),
'push_host' => env('PROMETHEUS_PUSH_HOST', '0.0.0.0'),
'push_port' => env('PROMETHEUS_PUSH_PORT', '9091'),
'push_interval' => env('PROMETHEUS_PUSH_INTERVAL', 5),
],
'statsd' => [
'driver' => Hyperf\Metric\Adapter\StatsD\MetricFactory::class,
'namespace' => env('APP_NAME', 'skeleton'),
'udp_host' => env('STATSD_UDP_HOST', '127.0.0.1'),
'udp_port' => env('STATSD_UDP_PORT', '8125'),
'enable_batch' => env('STATSD_ENABLE_BATCH', true),
'push_interval' => env('STATSD_PUSH_INTERVAL', 5),
'sample_rate' => env('STATSD_SAMPLE_RATE', 1.0),
],
'influxdb' => [
'driver' => Hyperf\Metric\Adapter\InfluxDB\MetricFactory::class,
'namespace' => env('APP_NAME', 'skeleton'),
'host' => env('INFLUXDB_HOST', '127.0.0.1'),
'port' => env('INFLUXDB_PORT', '8086'),
'username' => env('INFLUXDB_USERNAME', ''),
'password' => env('INFLUXDB_PASSWORD', ''),
'dbname' => env('INFLUXDB_DBNAME', true),
'push_interval' => env('INFLUXDB_PUSH_INTERVAL', 5),
'auto_create_db' => env('INFLUXDB_AUTO_CREATE_DB', true),
],
],
];

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\InfluxDB;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\ClientFactory as GuzzleClientFactory;
use Hyperf\Metric\Adapter\Prometheus\Counter;
use Hyperf\Metric\Adapter\Prometheus\Gauge;
use Hyperf\Metric\Adapter\Prometheus\Histogram;
use Hyperf\Metric\Contract\CounterInterface;
use Hyperf\Metric\Contract\GaugeInterface;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use InfluxDB\Client;
use InfluxDB\Database;
use InfluxDB\Database\RetentionPolicy;
use InfluxDB\Driver\DriverInterface;
use InfluxDB\Point;
use Prometheus\CollectorRegistry;
use Prometheus\Sample;
use Swoole\Coroutine;
class MetricFactory implements MetricFactoryInterface
{
/**
* @var ConfigInterface
*/
private $config;
/**
* @var CollectorRegistry
*/
private $registry;
/**
* @var guzzleClientFactory
*/
private $guzzleClientFactory;
/**
* @var string
*/
private $name;
public function __construct(ConfigInterface $config, CollectorRegistry $registry, GuzzleClientFactory $guzzleClientFactory)
{
$this->config = $config;
$this->registry = $registry;
$this->guzzleClientFactory = $guzzleClientFactory;
$this->name = $this->config->get('metric.default');
}
public function makeCounter(string $name, ?array $labelNames = []): CounterInterface
{
return new Counter(
$this->registry,
$this->getNamespace(),
$name,
'count ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function makeGauge(string $name, ?array $labelNames = []): GaugeInterface
{
return new Gauge(
$this->registry,
$this->getNamespace(),
$name,
'gauge ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function makeHistogram(string $name, ?array $labelNames = []): HistogramInterface
{
return new Histogram(
$this->registry,
$this->getNamespace(),
$name,
'measure ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function handle(): void
{
$host = $this->config->get("metric.metric.{$this->name}.host");
$port = $this->config->get("metric.metric.{$this->name}.port");
$username = $this->config->get("metric.metric.{$this->name}.username");
$password = $this->config->get("metric.metric.{$this->name}.password");
$dbname = $this->config->get("metric.metric.{$this->name}.dbname");
$interval = (float) $this->config->get("metric.metric.{$this->name}.push_interval", 5);
$create = $this->config->get("metric.metric.{$this->name}.auto_create_db");
$client = new Client($host, $port, $username, $password);
$guzzleClient = $this->guzzleClientFactory->create([
'connect_timeout' => $client->getConnectTimeout(),
'timeout' => $client->getTimeout(),
'base_uri' => $client->getBaseURI(),
'verify' => $client->getVerifySSL(),
]);
$client->setDriver(make(DriverInterface::class, ['client' => $guzzleClient]));
$database = $client->selectDB($dbname);
if (! $database->exists() && $create) {
$database->create(new RetentionPolicy($dbname, '1d', 1, true));
}
while (true) {
Coroutine::sleep($interval);
$points = [];
$metrics = $this->registry->getMetricFamilySamples();
foreach ($metrics as $metric) {
foreach ($metric->getSamples() as $sample) {
$points[] = $this->createPoint($sample);
}
}
$result = $database->writePoints($points, Database::PRECISION_SECONDS);
}
}
protected function createPoint(Sample $sample): Point
{
return new Point(
$sample->getName(),
$sample->getValue(),
$labels = array_combine($sample->getLabelNames(), $sample->getLabelValues()),
[],
time()
);
}
private function getNamespace(): string
{
return $this->config->get("metric.metric.{$this->name}.namespace");
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\Prometheus;
class Constants
{
const SCRAPE_MODE = 1;
const PUSH_MODE = 2;
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\Prometheus;
use Hyperf\Metric\Contract\CounterInterface;
use Prometheus\CollectorRegistry;
class Counter implements CounterInterface
{
/**
* @var \Prometheus\CollectorRegistry
*/
protected $registry;
/**
* @var \Prometheus\Counter
*/
protected $counter;
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(CollectorRegistry $registry, string $namespace, string $name, string $help, array $labelNames)
{
$this->registry = $registry;
$this->counter = $registry->getOrRegisterCounter($namespace, $name, $help, $labelNames);
}
public function with(string ...$labelValues): CounterInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function add(int $delta): void
{
$this->counter->incBy($delta, $this->labelValues);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\Prometheus;
use Hyperf\Metric\Contract\GaugeInterface;
class Gauge implements GaugeInterface
{
/**
* @var \Prometheus\CollectorRegistry
*/
protected $registry;
/**
* @var \Prometheus\Gauge
*/
protected $gauge;
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(\Prometheus\CollectorRegistry $registry, string $namespace, string $name, string $help, array $labelNames)
{
$this->registry = $registry;
$this->gauge = $registry->getOrRegisterGauge($namespace, $name, $help, $labelNames);
}
public function with(string ...$labelValues): GaugeInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function set(float $value): void
{
$this->gauge->set($value, $this->labelValues);
}
public function add(float $delta): void
{
$this->gauge->incBy($delta, $this->labelValues);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\Prometheus;
use Hyperf\Metric\Contract\HistogramInterface;
use Prometheus\CollectorRegistry;
class Histogram implements HistogramInterface
{
/**
* @var \Prometheus\CollectorRegistry
*/
protected $registry;
/**
* @var \Prometheus\Histogram
*/
protected $histogram;
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(CollectorRegistry $registry, string $namespace, string $name, string $help, array $labelNames)
{
$this->registry = $registry;
$this->histogram = $registry->getOrRegisterHistogram($namespace, $name, $help, $labelNames);
}
public function with(string ...$labelValues): HistogramInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function put(float $sample): void
{
$this->histogram->observe($sample, $this->labelValues);
}
}

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\Prometheus;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\ClientFactory as GuzzleClientFactory;
use Hyperf\Metric\Contract\CounterInterface;
use Hyperf\Metric\Contract\GaugeInterface;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Exception\InvalidArgumentException;
use Hyperf\Metric\Exception\RuntimeException;
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;
use Swoole\Coroutine;
use Swoole\Coroutine\Http\Server;
class MetricFactory implements MetricFactoryInterface
{
/**
* @var ConfigInterface
*/
private $config;
/**
* @var CollectorRegistry
*/
private $registry;
/**
* @var guzzleClientFactory
*/
private $guzzleClientFactory;
/**
* @var string
*/
private $name;
public function __construct(ConfigInterface $config, CollectorRegistry $registry, GuzzleClientFactory $guzzleClientFactory)
{
$this->config = $config;
$this->registry = $registry;
$this->guzzleClientFactory = $guzzleClientFactory;
$this->name = $this->config->get('metric.default');
$this->guardConfig();
}
public function makeCounter(string $name, ?array $labelNames = []): CounterInterface
{
return new Counter(
$this->registry,
$this->getNamespace(),
$name,
'count ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function makeGauge(string $name, ?array $labelNames = []): GaugeInterface
{
return new Gauge(
$this->registry,
$this->getNamespace(),
$name,
'gauge ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function makeHistogram(string $name, ?array $labelNames = []): HistogramInterface
{
return new Histogram(
$this->registry,
$this->getNamespace(),
$name,
'measure ' . str_replace('_', ' ', $name),
$labelNames
);
}
public function handle(): void
{
switch ($this->config->get("metric.metric.{$this->name}.mode")) {
case Constants::SCRAPE_MODE:
$this->scrapeHandle();
break;
case Constants::PUSH_MODE:
$this->pushHandle();
break;
default:
throw new InvalidArgumentException('Unsupported Prometheus mode encountered');
break;
}
}
protected function scrapeHandle()
{
$host = $this->config->get("metric.metric.{$this->name}.scrape_host");
$port = $this->config->get("metric.metric.{$this->name}.scrape_port");
$path = $this->config->get("metric.metric.{$this->name}.scrape_path");
$renderer = new RenderTextFormat();
$server = new Server($host, (int) $port, false);
$server->handle($path, function ($request, $response) use ($renderer) {
$response->header('Content-Type', RenderTextFormat::MIME_TYPE);
$response->end($renderer->render($this->registry->getMetricFamilySamples()));
});
$server->start();
}
protected function pushHandle()
{
while (true) {
$interval = (float) $this->config->get("metric.metric.{$this->name}.push_interval", 5);
$host = $this->config->get("metric.metric.{$this->name}.push_host");
$port = $this->config->get("metric.metric.{$this->name}.push_port");
$this->doRequest("{$host}:{$port}", $this->getNamespace(), 'put');
Coroutine::sleep($interval);
}
}
private function getNamespace(): string
{
return $this->config->get("metric.metric.{$this->name}.namespace");
}
private function guardConfig()
{
if ($this->config->get("metric.metric.{$this->name}.mode") == Constants::SCRAPE_MODE &&
$this->config->get('metric.use_standalone_process') == false) {
throw new RuntimeException(
"Prometheus in scrape mode must be used in conjunction with standalone process. \n Set metric.use_standalone_process to true to avoid this error."
);
}
}
private function doRequest(string $address, string $job, string $method)
{
$url = 'http://' . $address . '/metrics/job/' . $job . '/ip/' . current(swoole_get_local_ip()) . '/pid/' . getmypid();
$client = $this->guzzleClientFactory->create();
$requestOptions = [
'headers' => [
'Content-Type' => RenderTextFormat::MIME_TYPE,
],
'connect_timeout' => 10,
'timeout' => 20,
];
if ($method != 'delete') {
$renderer = new RenderTextFormat();
$requestOptions['body'] = $renderer->render($this->registry->getMetricFamilySamples());
}
$response = $client->request($method, $url, $requestOptions);
$statusCode = $response->getStatusCode();
if ($statusCode != 200 && $statusCode != 202) {
$msg = 'Unexpected status code ' . $statusCode . ' received from pushgateway ' . $address . ': ' . $response->getBody();
throw new RuntimeException($msg);
}
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\RemoteProxy;
use Hyperf\Metric\Contract\CounterInterface;
use Hyperf\Process\ProcessCollector;
class Counter implements CounterInterface
{
/**
* @var string
*/
protected const TARGET_PROCESS_NAME = 'metric';
/**
* @var string
*/
public $name;
/**
* @var string[];
*/
public $labelNames = [];
/**
* @var string[]
*/
public $labelValues = [];
/**
* @var int
*/
public $delta;
public function __construct(string $name, array $labelNames)
{
$this->name = $name;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): CounterInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function add(int $delta): void
{
$this->delta = $delta;
$process = ProcessCollector::get(static::TARGET_PROCESS_NAME)[0];
$process->write(serialize($this));
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\RemoteProxy;
use Hyperf\Metric\Contract\GaugeInterface;
use Hyperf\Process\ProcessCollector;
class Gauge implements GaugeInterface
{
/**
* @var string
*/
protected const TARGET_PROCESS_NAME = 'metric';
/**
* @var string
*/
public $name;
/**
* @var string[];
*/
public $labelNames = [];
/**
* @var string[]
*/
public $labelValues = [];
/**
* @var null|float
*/
public $delta;
/**
* @var null|float
*/
public $value;
public function __construct(string $name, array $labelNames)
{
$this->name = $name;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): GaugeInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function set(float $value): void
{
$this->value = $value;
$this->delta = null;
$process = ProcessCollector::get(static::TARGET_PROCESS_NAME)[0];
$process->write(serialize($this));
}
public function add(float $delta): void
{
$this->delta = $delta;
$this->value = null;
$process = ProcessCollector::get(static::TARGET_PROCESS_NAME)[0];
$process->write(serialize($this));
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\RemoteProxy;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Process\ProcessCollector;
class Histogram implements HistogramInterface
{
/**
* @var string
*/
protected const TARGET_PROCESS_NAME = 'metric';
/**
* @var string
*/
public $name;
/**
* @var string[];
*/
public $labelNames = [];
/**
* @var string[]
*/
public $labelValues = [];
/**
* @var float
*/
public $sample;
public function __construct(string $name, array $labelNames)
{
$this->name = $name;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): HistogramInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function put(float $sample): void
{
$this->sample = $sample;
$process = ProcessCollector::get(static::TARGET_PROCESS_NAME)[0];
$process->write(serialize($this));
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\RemoteProxy;
use Hyperf\Metric\Contract\CounterInterface;
use Hyperf\Metric\Contract\GaugeInterface;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Exception\RuntimeException;
class MetricFactory implements MetricFactoryInterface
{
public function makeCounter(string $name, ?array $labelNames = []): CounterInterface
{
return new Counter(
$name,
$labelNames
);
}
public function makeGauge(string $name, ?array $labelNames = []): GaugeInterface
{
return new Gauge(
$name,
$labelNames
);
}
public function makeHistogram(string $name, ?array $labelNames = []): HistogramInterface
{
return new Histogram(
$name,
$labelNames
);
}
public function handle(): void
{
throw new RuntimeException('RemoteProxy adapter cannot handle metrics reporting/serving directly');
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\StatsD;
use Domnikl\Statsd\Client;
use Hyperf\Metric\Contract\CounterInterface;
class Counter implements CounterInterface
{
/**
* @var \Domnikl\Statsd\Client
*/
protected $client;
/**
* @var string
*/
protected $name;
/**
* @var float
*/
protected $sampleRate;
/**
* @var string[]
*/
protected $labelNames = [];
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(Client $client, string $name, float $sampleRate, array $labelNames)
{
$this->client = $client;
$this->name = $name;
$this->sampleRate = $sampleRate;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): CounterInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function add(int $delta): void
{
$this->client->count($this->name, $delta, $this->sampleRate, array_combine($this->labelNames, $this->labelValues));
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\StatsD;
use Domnikl\Statsd\Client;
use Hyperf\Metric\Contract\GaugeInterface;
class Gauge implements GaugeInterface
{
/**
* @var \Domnikl\Statsd\Client
*/
protected $client;
/**
* @var string
*/
protected $name;
/**
* @var float
*/
protected $sampleRate;
/**
* @var string[]
*/
protected $labelNames = [];
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(Client $client, string $name, float $sampleRate, array $labelNames)
{
$this->client = $client;
$this->name = $name;
$this->sampleRate = $sampleRate;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): GaugeInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function set(float $value): void
{
if ($value < 0) {
// StatsD gauge doesn't support negative values.
$value = 0;
}
$this->client->gauge($this->name, (string) $value, array_combine($this->labelNames, $this->labelValues));
}
public function add(float $delta): void
{
if ($delta >= 0) {
$deltaStr = '+' . $delta;
} else {
$deltaStr = (string) $delta;
}
$this->client->gauge($this->name, $deltaStr, array_combine($this->labelNames, $this->labelValues));
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\StatsD;
use Domnikl\Statsd\Client;
use Hyperf\Metric\Contract\HistogramInterface;
class Histogram implements HistogramInterface
{
/**
* @var \Domnikl\Statsd\Client
*/
protected $client;
/**
* @var string
*/
protected $name;
/**
* @var float
*/
protected $sampleRate;
/**
* @var string[]
*/
protected $labelNames = [];
/**
* @var string[]
*/
protected $labelValues = [];
public function __construct(Client $client, string $name, float $sampleRate, array $labelNames)
{
$this->client = $client;
$this->name = $name;
$this->sampleRate = $sampleRate;
$this->labelNames = $labelNames;
}
public function with(string ...$labelValues): HistogramInterface
{
$this->labelValues = $labelValues;
return $this;
}
public function put(float $sample): void
{
$this->client->timing($this->name, $sample, $this->sampleRate, array_combine($this->labelNames, $this->labelValues));
}
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Adapter\StatsD;
use Domnikl\Statsd\Client;
use Domnikl\Statsd\Connection;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Metric\Contract\CounterInterface;
use Hyperf\Metric\Contract\GaugeInterface;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Swoole\Coroutine;
class MetricFactory implements MetricFactoryInterface
{
/**
* @var ConfigInterface
*/
private $config;
/**
* @var Client
*/
private $client;
/**
* GuzzleClientFactory.
*/
private $guzzleClientFactory;
public function __construct(ConfigInterface $config)
{
$this->config = $config;
$this->client = make(Client::class, [
'connection' => $this->getConnection(),
'namespace' => $this->getNamespace(),
'sampleRateAllMetrics' => $this->getSampleRate(),
]);
}
public function makeCounter(string $name, ?array $labelNames = []): CounterInterface
{
return new Counter(
$this->client,
$name,
$this->getSampleRate(),
$labelNames
);
}
public function makeGauge(string $name, ?array $labelNames = []): GaugeInterface
{
return new Gauge(
$this->client,
$name,
$this->getSampleRate(),
$labelNames
);
}
public function makeHistogram(string $name, ?array $labelNames = []): HistogramInterface
{
return new Histogram(
$this->client,
$name,
$this->getSampleRate(),
$labelNames
);
}
public function handle(): void
{
$name = $this->config->get('metric.default');
$interval = (float) $this->config->get("metric.metric.{$name}.push_interval", 5);
$batchEnabled = $this->config->get("metric.metric.{$name}.enable_batch") == true;
// Block handle from returning.
do {
if ($batchEnabled) {
$this->client->startBatch();
Coroutine::sleep((int) $interval);
$this->client->endBatch();
} else {
Coroutine::sleep(5000);
}
} while (true);
}
protected function getConnection(): Connection
{
$name = $this->config->get('metric.default');
$host = $this->config->get("metric.metric.{$name}.udp_host");
$port = $this->config->get("metric.metric.{$name}.udp_port");
return make(Connection::class, [
'host' => $host,
'port' => (int) $port,
'timeout' => null,
'persistent' => true,
]);
}
protected function getNamespace(): string
{
$name = $this->config->get('metric.default');
return $this->config->get("metric.metric.{$name}.namespace");
}
protected function getSampleRate(): float
{
$name = $this->config->get('metric.default');
return $this->config->get("metric.metric.{$name}.sample_rate", 1.0);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Hyperf\Di\Annotation\AbstractAnnotation;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
class Counter extends AbstractAnnotation
{
/**
* @var string
*/
public $name = '';
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Hyperf\Di\Annotation\AbstractAnnotation;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
class Histogram extends AbstractAnnotation
{
/**
* @var string
*/
public $name = '';
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Aspect;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AroundInterface;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Metric\Annotation\Counter;
use Hyperf\Metric\Contract\MetricFactoryInterface;
/**
* @Aspect
*/
class CounterAnnotationAspect implements AroundInterface
{
public $classes = [];
public $annotations = [
Counter::class,
];
/**
* @var MetricFactoryInterface
*/
private $factory;
public function __construct(MetricFactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* @return mixed return the value from process method of ProceedingJoinPoint, or the value that you handled
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
$metadata = $proceedingJoinPoint->getAnnotationMetadata();
$source = $this->fromCamelCase($proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName);
/** @var Counter $annotation */
if ($annotation = $metadata->method[Counter::class] ?? null) {
$name = $annotation->name ?: $source;
} else {
$name = $source;
}
$counter = $this->factory->makeCounter($name, ['class', 'method']);
$result = $proceedingJoinPoint->process();
$counter
->with(
$proceedingJoinPoint->className,
$proceedingJoinPoint->methodName
)
->add(1);
return $result;
}
private function fromCamelCase(string $input): string
{
preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
$ret = $matches[0];
foreach ($ret as &$match) {
$match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
}
return implode('_', $ret);
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Aspect;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AroundInterface;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Metric\Annotation\Histogram;
use Hyperf\Metric\Timer;
/**
* @Aspect
*/
class HistogramAnnotationAspect implements AroundInterface
{
public $classes = [];
public $annotations = [
Histogram::class,
];
/**
* @return mixed return the value from process method of ProceedingJoinPoint, or the value that you handled
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
$metadata = $proceedingJoinPoint->getAnnotationMetadata();
$source = $this->fromCamelCase($proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName);
/** @var Histogram $annotation */
if ($annotation = $metadata->method[Histogram::class] ?? null) {
$name = $annotation->name ?: $source;
} else {
$name = $source;
}
$timer = new Timer(
$name,
[
'class' => $proceedingJoinPoint->className,
'method' => $proceedingJoinPoint->methodName,
]
);
return $proceedingJoinPoint->process();
}
private function fromCamelCase(string $input): string
{
preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
$ret = $matches[0];
foreach ($ret as &$match) {
$match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
}
return implode('_', $ret);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric;
use Domnikl\Statsd\Connection;
use Domnikl\Statsd\Connection\UdpSocket;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Listener\OnMetricFactoryReady;
use Hyperf\Metric\Listener\OnPipeMessage;
use Hyperf\Metric\Listener\OnWorkerStart;
use InfluxDB\Driver\DriverInterface;
use InfluxDB\Driver\Guzzle;
use Prometheus\Storage\Adapter;
use Prometheus\Storage\InMemory;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
MetricFactoryInterface::class => MetricFactoryPicker::class,
Adapter::class => InMemory::class,
Connection::class => UdpSocket::class,
DriverInterface::class => Guzzle::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
'publish' => [
[
'id' => 'config',
'description' => 'The config for metric component.',
'source' => __DIR__ . '/../publish/metric.php',
'destination' => BASE_PATH . '/config/autoload/metric.php',
],
],
'listeners' => [
OnPipeMessage::class,
OnMetricFactoryReady::class,
OnWorkerStart::class,
],
];
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Contract;
/**
* Counter describes a metric that accumulates values monotonically.
* An example of a counter is the number of received HTTP requests.
*/
interface CounterInterface
{
public function with(string ...$labelValues): self;
public function add(int $delta): void;
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Contract;
/**
* Gauge describes a metric that takes specific values over time.
* An example of a gauge is the current depth of a job queue.
*/
interface GaugeInterface
{
public function with(string ...$labelValues): self;
public function set(float $value): void;
public function add(float $delta): void;
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Contract;
/**
* Histogram describes a metric that takes repeated observations of the same
* kind of thing, and produces a statistical summary of those observations,
* typically expressed as quantiles or buckets. An example of a histogram is
* HTTP request latencies.
*/
interface HistogramInterface
{
public function with(string ...$labelValues): self;
public function put(float $sample): void;
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Contract;
interface MetricFactoryInterface
{
/**
* Create a Counter.
* @param string $name name of the metric
* @param string[] $labelNames key of your label kvs
* @return CounterInterface
*/
public function makeCounter(string $name, ?array $labelNames = []): CounterInterface;
/**
* Create a Gauge.
* @param string $name name of the metric
* @param string[] $labelNames key of your label kvs
* @return GaugeInterface
*/
public function makeGauge(string $name, ?array $labelNames = []): GaugeInterface;
/**
* Create a HistogramInterface.
* @param string $name name of the metric
* @param string[] $labelNames key of your label kvs
* @return HistogramInterface
*/
public function makeHistogram(string $name, ?array $labelNames = []): HistogramInterface;
/**
* Handle the metric collecting/reporting/serving tasks.
*/
public function handle(): void;
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Event;
use Hyperf\Metric\Contract\MetricFactoryInterface;
class MetricFactoryReady
{
/**
* A ready to use factory.
* @var MetricFactoryInterface
*/
public $factory;
public function __construct(MetricFactoryInterface $factory)
{
$this->factory = $factory;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Exception;
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Exception;
class RuntimeException extends \RuntimeException
{
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DbConnection\Pool\PoolFactory;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeWorkerStart;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Psr\Container\ContainerInterface;
use Swoole\Timer;
/**
* A simple mysql connection watcher served as an example.
* This listener is not auto enabled.Tweak it to fit your
* own need.
*/
class DBPoolWatcher implements ListenerInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
BeforeWorkerStart::class,
];
}
/**
* Periodically scan metrics.
*/
public function process(object $event)
{
$workerId = $event->workerId;
$pool = $this
->container
->get(PoolFactory::class)
->getPool('default');
$gauge = $this
->container
->get(MetricFactoryInterface::class)
->makeGauge('mysql_connections_in_use', ['pool', 'worker'])
->with('default', (string) $workerId);
$config = $this->container->get(ConfigInterface::class);
$timerInterval = $config->get('metric.default_metric_interval', 5);
Timer::tick($timerInterval * 1000, function () use ($gauge, $pool) {
$gauge->set((float) $pool->getCurrentConnections());
});
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Event\MetricFactoryReady;
use Hyperf\Metric\MetricSetter;
use Psr\Container\ContainerInterface;
use Swoole\Coroutine;
use Swoole\Server;
use Swoole\Timer;
/**
* Similar to OnWorkerStart, but this only runs in one process.
*/
class OnMetricFactoryReady implements ListenerInterface
{
use MetricSetter;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var MetricFactoryInterface
*/
protected $factory;
/**
* @var ConfigInterface
*/
private $config;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->config = $container->get(ConfigInterface::class);
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
MetricFactoryReady::class,
];
}
/**
* Periodically scan metrics.
*/
public function process(object $event)
{
if (! $this->config->get('metric.enable_default_metric')) {
return;
}
$this->factory = $event->factory;
$metrics = $this->factoryMetrics(
[],
'sys_load',
'event_num',
'signal_listener_num',
'aio_task_num',
'aio_worker_num',
'c_stack_size',
'coroutine_num',
'coroutine_peak_num',
'coroutine_last_cid',
'connection_num',
'accept_count',
'close_count',
'worker_num',
'idle_worker_num',
'tasking_num',
'request_count',
'timer_num',
'timer_round'
);
$server = $this->container->get(Server::class);
$timerInterval = $this->config->get('metric.default_metric_interval', 5);
Timer::tick($timerInterval * 1000, function () use ($metrics, $server) {
$serverStats = $server->stats();
$coroutineStats = Coroutine::stats();
$timerStats = Timer::stats();
$this->trySet('', $metrics, $serverStats);
$this->trySet('', $metrics, $coroutineStats);
$this->trySet('timer_', $metrics, $timerStats);
$load = sys_getloadavg();
$metrics['sys_load']->set(round($load[0] / swoole_cpu_num(), 2));
});
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Metric\Adapter\RemoteProxy\Counter;
use Hyperf\Metric\Adapter\RemoteProxy\Gauge;
use Hyperf\Metric\Adapter\RemoteProxy\Histogram;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Process\Event\PipeMessage;
/**
* Receives messages in metric process.
*/
class OnPipeMessage implements ListenerInterface
{
/**
* @var MetricFactoryInterface
*/
private $factory;
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
PipeMessage::class,
];
}
/**
* Handle the Event when the event is triggered, all listeners will
* complete before the event is returned to the EventDispatcher.
*/
public function process(object $event)
{
$this->factory = make(MetricFactoryInterface::class);
if (property_exists($event, 'data') && $event instanceof PipeMessage) {
$inner = $event->data;
switch (true) {
case $inner instanceof Counter:
$counter = $this->factory->makeCounter($inner->name, $inner->labelNames);
$counter->with(...$inner->labelValues)->add($inner->delta);
break;
case $inner instanceof Gauge:
$gauge = $this->factory->makeGauge($inner->name, $inner->labelNames);
if (isset($inner->value)) {
$gauge->with(...$inner->labelValues)->set($inner->value);
} else {
$gauge->with(...$inner->labelValues)->add($inner->delta);
}
break;
case $inner instanceof Histogram:
$histogram = $this->factory->makeHistogram($inner->name, $inner->labelNames);
$histogram->with(...$inner->labelValues)->put($inner->sample);
break;
default:
// Nothing to do
break;
}
}
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeWorkerStart;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Event\MetricFactoryReady;
use Hyperf\Metric\MetricSetter;
use Hyperf\Utils\Coroutine;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Swoole\Server;
use Swoole\Timer;
use Throwable;
use function gc_status;
use function getrusage;
use function memory_get_peak_usage;
use function memory_get_usage;
/**
* Collect and handle metrics before worker start.
*/
class OnWorkerStart implements ListenerInterface
{
use MetricSetter;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var MetricFactoryInterface
*/
protected $factory;
/**
* @var ConfigInterface
*/
private $config;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->config = $container->get(ConfigInterface::class);
$this->factory = $container->get(MetricFactoryInterface::class);
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
BeforeWorkerStart::class,
];
}
/**
* Handle the Event when the event is triggered, all listeners will
* complete before the event is returned to the EventDispatcher.
*/
public function process(object $event)
{
$workerId = $event->workerId;
if ($workerId === null) {
return;
}
/*
* If no standalone process is started, we have to handle metrics on worker.
*/
if (! $this->config->get('metric.use_standalone_process', true)) {
$this->spawnHandle();
}
/*
* Allow user to hook up their own metrics logic
*/
if ($this->shouldFireMetricFactoryReadyEvent($workerId)) {
$eventDispatcher = $this->container->get(EventDispatcherInterface::class);
$eventDispatcher->dispatch(new MetricFactoryReady($this->factory));
}
if (! $this->config->get('metric.enable_default_metric', false)) {
return;
}
// The following metrics MUST be collected in worker.
$metrics = $this->factoryMetrics(
['worker' => (string) $workerId],
'worker_request_count',
'worker_dispatch_count',
'memory_usage',
'memory_peak_usage',
'gc_runs',
'gc_collected',
'gc_threshold',
'gc_roots',
'ru_oublock',
'ru_inblock',
'ru_msgsnd',
'ru_msgrcv',
'ru_maxrss',
'ru_ixrss',
'ru_idrss',
'ru_minflt',
'ru_majflt',
'ru_nsignals',
'ru_nvcsw',
'ru_nivcsw',
'ru_nswap',
'ru_utime_tv_usec',
'ru_utime_tv_sec',
'ru_stime_tv_usec',
'ru_stime_tv_sec'
);
$server = $this->container->get(Server::class);
$timerInterval = $this->config->get('metric.default_metric_interval', 5);
Timer::tick($timerInterval * 1000, function () use ($metrics, $server) {
$serverStats = $server->stats();
if (function_exists('gc_status')) {
$this->trySet('gc_', $metrics, gc_status());
}
$this->trySet('', $metrics, getrusage());
$metrics['worker_request_count']->set($serverStats['worker_request_count']);
$metrics['worker_dispatch_count']->set($serverStats['worker_dispatch_count']);
$metrics['memory_usage']->set(memory_get_usage());
$metrics['memory_peak_usage']->set(memory_get_peak_usage());
});
}
private function shouldFireMetricFactoryReadyEvent(int $workerId): bool
{
return (! $this->config->get('metric.use_standalone_process'))
&& $workerId == 0;
}
private function spawnHandle()
{
Coroutine::create(function () {
try {
$this->factory->handle();
} catch (Throwable $t) {
$this->spawnHandle();
throw $t;
}
});
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Metric\Event\MetricFactoryReady;
use Psr\Container\ContainerInterface;
use Swoole\Timer;
/**
* A simple redis queue watcher served as an example.
* This listener is not auto enabled.Tweak it to fit your
* own need.
*/
class QueueWatcher implements ListenerInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
MetricFactoryReady::class,
];
}
/**
* Periodically scan metrics.
*/
public function process(object $event)
{
$queue = $this->container->get(DriverFactory::class)->get('default');
$waiting = $event
->factory
->makeGauge('queue_waiting', ['queue'])
->with('default');
$delayed = $event
->factory
->makeGauge('queue_delayed', ['queue'])
->with('default');
$failed = $event
->factory
->makeGauge('queue_failed', ['queue'])
->with('default');
$timeout = $event
->factory
->makeGauge('queue_timeout', ['queue'])
->with('default');
$config = $this->container->get(ConfigInterface::class);
$timerInterval = $config->get('metric.default_metric_interval', 5);
Timer::tick($timerInterval * 1000, function () use ($waiting, $delayed, $failed, $timeout, $queue) {
$info = $queue->info();
$waiting->set((float) $info['waiting']);
$delayed->set((float) $info['delayed']);
$failed->set((float) $info['failed']);
$timeout->set((float) $info['timeout']);
});
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Listener;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeWorkerStart;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Redis\Pool\PoolFactory;
use Psr\Container\ContainerInterface;
use Swoole\Timer;
/**
* A simple mysql connection watcher served as an example.
* This listener is not auto enabled.Tweak it to fit your
* own need.
*/
class RedisPoolWatcher implements ListenerInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
BeforeWorkerStart::class,
];
}
/**
* Periodically scan metrics.
*/
public function process(object $event)
{
$workerId = $event->workerId;
$pool = $this
->container
->get(PoolFactory::class)
->getPool('default');
$gauge = $this
->container
->get(MetricFactoryInterface::class)
->makeGauge('redis_connections_in_use', ['pool', 'worker'])
->with('default', (string) $workerId);
$config = $this->container->get(ConfigInterface::class);
$timerInterval = $config->get('metric.default_metric_interval', 5);
Timer::tick($timerInterval * 1000, function () use ($gauge, $pool) {
$gauge->set((float) $pool->getCurrentConnections());
});
}
}

60
src/metric/src/Metric.php Normal file
View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric;
use Hyperf\Metric\Contract\MetricFactoryInterface;
/**
* A Facade-like, syntax sugar class to create one-off metrics
* Beta Feature. API may change.
*/
class Metric
{
public static function count(string $name, ?int $delta = 1, ?array $labels = [])
{
make(MetricFactoryInterface::class)
->makeCounter($name, array_keys($labels))
->with(...array_values($labels))
->add($delta);
}
public static function gauge(string $name, float $value, ?array $labels = [])
{
make(MetricFactoryInterface::class)
->makeGauge($name, array_keys($labels))
->with(...array_values($labels))
->set($value);
}
public static function shift(string $name, float $delta, ?array $labels = [])
{
make(MetricFactoryInterface::class)
->makeGauge($name, array_keys($labels))
->with(...array_values($labels))
->add($delta);
}
public static function put(string $name, float $sample, ?array $labels = [])
{
make(MetricFactoryInterface::class)
->makeHistogram($name, array_keys($labels))
->with(...array_values($labels))
->put($sample);
}
public static function time(string $name, callable $func, ?array $args = [], ?array $labels = [])
{
$timer = new Timer($name, $labels);
return $func(...$args);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Metric\Adapter\Prometheus\MetricFactory as PrometheusFactory;
use Hyperf\Metric\Adapter\RemoteProxy\MetricFactory as RemoteFactory;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Exception\InvalidArgumentException;
use Psr\Container\ContainerInterface;
class MetricFactoryPicker
{
/**
* @var bool
*/
public static $inMetricProcess = false;
public function __invoke(ContainerInterface $container)
{
$config = $container->get(ConfigInterface::class);
$useStandaloneProcess = $config->get('metric.use_standalone_process');
// Return a proxy object for workers if user wants to use a dedicated metric process.
if ($useStandaloneProcess && ! static::$inMetricProcess) {
return $container->get(RemoteFactory::class);
}
$name = $config->get('metric.default');
$dedicatedProcess = $config->get('metric.metric.use_standalone_process');
$driver = $config->get("metric.metric.{$name}.driver", PrometheusFactory::class);
$factory = $container->get($driver);
if (! ($factory instanceof MetricFactoryInterface)) {
throw new InvalidArgumentException(
sprintf('The driver %s is not a valid factory.', $driver)
);
}
return $factory;
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric;
use Hyperf\Metric\Contract\GaugeInterface;
/**
* A Helper trait to set stats from swoole and kernal.
*/
trait MetricSetter
{
/**
* Try to set every stats available to the gauge.
* Some of the stats might be missing depending
* on the platform.
* @param string $prefix
* @param array $metrics
* @param array $stats
*/
private function trySet(string $prefix, array $metrics, array $stats): void
{
foreach (array_keys($stats) as $key) {
$metricsKey = \str_replace('.', '_', $prefix . $key);
if (array_key_exists($metricsKey, $metrics)) {
$metrics[$metricsKey]->set($stats[$key]);
}
}
}
/**
* Create an array of gauges.
* @param array<string, string> $labels
* @param array<int, string> $names
* @return GaugeInterface[]
*/
private function factoryMetrics(array $labels, string ...$names): array
{
$out = [];
foreach ($names as $name) {
$out[$name] = $this
->factory
->makeGauge($name, \array_keys($labels))
->with(...\array_values($labels));
}
return $out;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Middleware;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Timer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class MetricMiddleware implements MiddlewareInterface
{
/**
* @var MetricFactoryInterface
*/
private $factory;
public function __construct(MetricFactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* Process an incoming server request.
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$labels = [
'request_status' => '500', //default to 500 incase uncaught exception occur
'request_path' => $request->getRequestTarget(),
'request_method' => $request->getMethod(),
];
$timer = new Timer('http_requests', $labels);
$response = $handler->handle($request);
$labels['request_status'] = (string) $response->getStatusCode();
$timer->end($labels);
return $response;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric\Process;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Event\MetricFactoryReady;
use Hyperf\Metric\MetricFactoryPicker;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* Metric Process.
* @Process
*/
class MetricProcess extends AbstractProcess
{
public $name = 'metric';
public $nums = 1;
/**
* @var MetricFactoryInterface
*/
protected $factory;
public function isEnable(): bool
{
$config = $this->container->get(ConfigInterface::class);
return $config->get('metric.use_standalone_process') ?? false;
}
public function handle(): void
{
MetricFactoryPicker::$inMetricProcess = true;
$this->factory = make(MetricFactoryInterface::class);
$this
->container
->get(EventDispatcherInterface::class)
->dispatch(new MetricFactoryReady($this->factory));
$this
->factory
->handle();
}
}

74
src/metric/src/Timer.php Normal file
View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Metric;
use Hyperf\Metric\Contract\MetricFactoryInterface;
/**
* Syntax sugar class to handle time.
*/
class Timer
{
/**
* @var string
*/
protected $name;
/**
* @var array<string,string>
*/
protected $labels;
/**
* @var float
*/
protected $time;
/**
* @var bool
*/
private $ended = false;
public function __construct(string $name, ?array $default = [])
{
$this->name = $name;
$this->labels = $default;
$this->time = microtime(true);
}
public function __destruct()
{
$this->end();
}
public function end(?array $labels = []): void
{
if ($this->ended) {
return;
}
foreach ($labels as $key => $value) {
if (array_key_exists($key, $this->labels)) {
$this->labels[$key] = $value;
}
}
$histogram = make(MetricFactoryInterface::class)
->makeHistogram($this->name, array_keys($this->labels))
->with(...array_values($this->labels));
$d = (float) microtime(true) - $this->time;
if ($d < 0) {
$d = (float) 0;
}
$histogram->put($d);
$this->ended = true;
}
}

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Metric\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Container;
use Hyperf\Metric\Adapter\Prometheus\MetricFactory as PrometheusFactory;
use Hyperf\Metric\Adapter\RemoteProxy\MetricFactory as RemoteFactory;
use Hyperf\Metric\Adapter\StatsD\MetricFactory as StatsDFactory;
use Hyperf\Metric\MetricFactoryPicker;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class MetricFactoryPickerTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
public function testPrometheus()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => false,
'enable_default_metrics' => true,
],
]);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(PrometheusFactory::class)->andReturn(Mockery::mock(PrometheusFactory::class));
$picker = new MetricFactoryPicker();
$this->assertInstanceOf(PrometheusFactory::class, $picker($container));
}
public function testStatsD()
{
$config = new Config([
'metric' => [
'default' => 'statsD',
'use_standalone_process' => false,
'enable_default_metrics' => true,
'metric' => [
'prometheus' => [
'driver' => PrometheusFactory::class,
],
'statsD' => [
'driver' => StatsDFactory::class,
],
],
],
]);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(StatsDFactory::class)->andReturn(Mockery::mock(StatsDFactory::class));
$picker = new MetricFactoryPicker();
$this->assertInstanceOf(StatsDFactory::class, $picker($container));
}
public function testProxy()
{
$config = new Config([
'metric' => [
'default' => 'statsD',
'use_standalone_process' => true,
'enable_default_metrics' => true,
'metric' => [
'prometheus' => [
'driver' => PrometheusFactory::class,
],
'statsD' => [
'driver' => StatsDFactory::class,
],
],
],
]);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(RemoteFactory::class)->andReturn(Mockery::mock(RemoteFactory::class));
$picker = new MetricFactoryPicker();
$this->assertInstanceOf(RemoteFactory::class, $picker($container));
}
public function testMetricProcess()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => true,
'enable_default_metrics' => false,
'metric' => [
'prometheus' => [
'driver' => PrometheusFactory::class,
],
'statsD' => [
'driver' => StatsDFactory::class,
],
],
],
]);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(PrometheusFactory::class)->andReturn(Mockery::mock(PrometheusFactory::class));
MetricFactoryPicker::$inMetricProcess = true;
$picker = new MetricFactoryPicker();
$this->assertInstanceOf(PrometheusFactory::class, $picker($container));
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Metric\Cases;
use Hyperf\Config\Config;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Metric\Adapter\Prometheus\Constants;
use Hyperf\Metric\Adapter\Prometheus\MetricFactory as PrometheusFactory;
use Hyperf\Metric\Exception\RuntimeException;
use Mockery;
use PHPUnit\Framework\TestCase;
use Prometheus\CollectorRegistry;
/**
* @internal
* @coversNothing
*/
class MetricFactoryTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
public function testPrometheusThrows()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => false,
'metric' => [
'prometheus' => [
'driver' => PrometheusFactory::class,
'mode' => Constants::SCRAPE_MODE,
],
],
],
]);
$r = Mockery::mock(CollectorRegistry::class);
$c = Mockery::mock(ClientFactory::class);
$this->expectException(RuntimeException::class);
$p = new PrometheusFactory($config, $r, $c);
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Metric\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Container;
use Hyperf\Metric\Adapter\Prometheus\MetricFactory as PrometheusFactory;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Listener\OnWorkerStart;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @internal
* @coversNothing
*/
class OnWorkerStartTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
public function testHandle()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => false,
'enable_default_metrics' => false,
],
]);
$factory = Mockery::mock(PrometheusFactory::class);
$factory->shouldReceive('handle')->atLeast()->times(1);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(MetricFactoryInterface::class)->andReturn($factory);
$l = new OnWorkerStart($container);
$l->process(new class() {
public $workerId = 1;
});
$this->assertTrue(true);
}
public function testFireEvent()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => false,
'enable_default_metrics' => false,
],
]);
$factory = Mockery::mock(PrometheusFactory::class);
$container = Mockery::mock(Container::class);
$factory->shouldReceive('handle')->atLeast()->times(1);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(MetricFactoryInterface::class)->andReturn($factory);
$container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn(
new class() {
public function dispatch()
{
return true;
}
}
)->once();
$l = new OnWorkerStart($container);
$l->process(new class() {
public $workerId = 0;
});
$l->process(new class() {
public $workerId = 1;
});
$this->assertTrue(true);
}
public function testNotFireEvent()
{
$config = new Config([
'metric' => [
'default' => 'prometheus',
'use_standalone_process' => true,
'enable_default_metrics' => false,
],
]);
$factory = Mockery::mock(PrometheusFactory::class);
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(MetricFactoryInterface::class)->andReturn($factory);
$l = new OnWorkerStart($container);
$l->process(new class() {
public $workerId = 0;
});
$this->assertTrue(true);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Metric\Cases;
use Hyperf\Di\Container;
use Hyperf\Metric\Contract\HistogramInterface;
use Hyperf\Metric\Contract\MetricFactoryInterface;
use Hyperf\Metric\Timer;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class TimerTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
public function testEnd()
{
$this->mockContainer();
$timer = new Timer('test');
$timer->end();
$this->assertTrue(true);
}
public function testEndCalledTwice()
{
$this->mockContainer();
$timer2 = new Timer('test');
$timer2->end();
$timer2->end();
$this->assertTrue(true);
}
public function testEndNotCalled()
{
$this->mockContainer();
$timer3 = new Timer('test');
unset($timer3);
$this->assertTrue(true);
}
private function mockContainer()
{
$container = Mockery::mock(Container::class);
$container->shouldReceive('make')->with(MetricFactoryInterface::class, [])->andReturn(new class() {
public function makeHistogram($name, $labels)
{
$histogram = Mockery::mock(HistogramInterface::class);
$histogram->shouldReceive('with')->andReturn($histogram);
$histogram->shouldReceive('put')->once();
return $histogram;
}
});
ApplicationContext::setContainer($container);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

8
src/metric/tests/ci.ini Normal file
View File

@ -0,0 +1,8 @@
[opcache]
opcache.enable_cli=1
[redis]
extension = "redis.so"
[swoole]
extension = "swoole.so"

View File

@ -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

85
src/utils/src/Backoff.php Normal file
View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils;
class Backoff
{
/**
* Max backoff.
*/
private const CAP = 60 * 1000; // 1 minute
/**
* @var int
*/
private $firstMs;
/**
* Backoff interval.
* @var int
*/
private $currentMs;
/**
* @param int the first backoff in milliseconds
*/
public function __construct(int $firstMs = 0)
{
if ($firstMs < 0) {
throw new \InvalidArgumentException(
'first backoff interval must be greater or equal than 0'
);
}
if ($firstMs > Backoff::CAP) {
throw new \InvalidArgumentException(
sprintf(
'first backoff interval must be less or equal than %d milliseconds',
self::CAP
)
);
}
$this->firstMs = $firstMs;
$this->currentMs = $firstMs;
}
/**
* Sleep until the next execution.
*/
public function sleep(): void
{
if ($this->currentMs === 0) {
return;
}
usleep($this->currentMs * 1000);
// update backoff using Decorrelated Jitter
// see: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
$this->currentMs = rand($this->firstMs, $this->currentMs * 3);
if ($this->currentMs > self::CAP) {
$this->currentMs = self::CAP;
}
}
/**
* Get the next backoff for logging, etc.
* @return int next backoff
*/
public function nextBackoff(): int
{
return $this->currentMs;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils\Codec;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use InvalidArgumentException;
class Json
{
public static function encode($data, $options = JSON_UNESCAPED_UNICODE): string
{
if ($data instanceof Jsonable) {
return (string) $data;
}
if ($data instanceof Arrayable) {
$data = $data->toArray();
}
$json = json_encode($data, $options);
static::handleJsonError(json_last_error(), json_last_error_msg());
return $json;
}
public static function decode(string $json, $assoc = true)
{
$decode = json_decode($json, $assoc);
static::handleJsonError(json_last_error(), json_last_error_msg());
return $decode;
}
protected static function handleJsonError($lastError, $message)
{
if ($lastError === JSON_ERROR_NONE) {
return;
}
throw new InvalidArgumentException($message, $lastError);
}
}

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
use Hyperf\Utils\ApplicationContext; use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Arr; use Hyperf\Utils\Arr;
use Hyperf\Utils\Backoff;
use Hyperf\Utils\Collection; use Hyperf\Utils\Collection;
use Hyperf\Utils\Coroutine; use Hyperf\Utils\Coroutine;
use Hyperf\Utils\HigherOrderTapProxy; use Hyperf\Utils\HigherOrderTapProxy;
@ -67,11 +68,12 @@ if (! function_exists('retry')) {
* Retry an operation a given number of times. * Retry an operation a given number of times.
* *
* @param int $times * @param int $times
* @param int $sleep * @param int $sleep millisecond
* @throws \Throwable * @throws \Throwable
*/ */
function retry($times, callable $callback, $sleep = 0) function retry($times, callable $callback, $sleep = 0)
{ {
$backoff = new Backoff($sleep);
beginning: beginning:
try { try {
return $callback(); return $callback();
@ -79,9 +81,7 @@ if (! function_exists('retry')) {
if (--$times < 0) { if (--$times < 0) {
throw $e; throw $e;
} }
if ($sleep) { $backoff->sleep();
usleep($sleep * 1000);
}
goto beginning; goto beginning;
} }
} }

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Utils;
use Hyperf\Utils\Backoff;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @covers \Hyperf\Utils\Backoff
*/
class BackoffTest extends TestCase
{
public function testBackoff()
{
$backoff = new Backoff(1);
$backoff->sleep();
$firstTick = $backoff->nextBackoff();
$this->assertGreaterThanOrEqual(1, $firstTick);
$this->assertLessThanOrEqual(3, $firstTick);
$backoff->sleep();
$secondTick = $backoff->nextBackoff();
$this->assertGreaterThanOrEqual(1, $secondTick);
$this->assertLessThanOrEqual(3 * $firstTick, $secondTick);
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Utils\Codec;
use Hyperf\Utils\Codec\Json;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class JsonTest extends TestCase
{
public function testEncode()
{
$data = [
'name' => 'Hyperf',
];
$json = '{"name":"Hyperf"}';
$this->assertSame($json, Json::encode($data));
}
public function testDecode()
{
$data = [
'name' => 'Hyperf',
];
$json = '{"name":"Hyperf"}';
$this->assertSame($data, Json::decode($json));
}
public function testDecodeToObject()
{
$data = [
'name' => 'Hyperf',
];
$json = '{"name":"Hyperf"}';
$this->assertEquals((object) $data, Json::decode($json, false));
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Control character error, possibly incorrectly encoded
*/
public function testDecodeException()
{
$data = [
'name' => 'Hyperf',
];
$json = '{"name":"Hyperf}';
$this->assertSame($data, Json::decode($json));
}
public function testJsonEncodeInCoroutine()
{
$result = null;
go(function () use (&$result) {
$result = Json::encode([1, 2, 3]);
});
$this->assertSame('[1,2,3]', $result);
go(function () use (&$result) {
$result = Json::decode('[1,2,3]');
});
$this->assertSame([1, 2, 3], $result);
}
}