关于laravel:记录-Laravel-中-GraphQL-接口请求频率

3次阅读

共计 6242 个字符,预计需要花费 16 分钟才能阅读完成。

前言

起源: 通常在产品的运行过程, 咱们可能会做数据埋点, 以此来晓得用户触发的行为, 拜访了多少页面, 做了哪些操作, 来不便产品依据用户爱好的做不同的调整和举荐, 同样在服务端开发层面, 也要做好“数据埋点”, 去记录接口的响应时长、接口调用频率, 参数频率等, 不便咱们从后端角度去剖析和优化问题, 如果遇到异样行为或者大量攻打起源, 咱们能够具体针对到某个接口去进行优化。

我的项目环境:

  • framework:laravel 5.8+
  • cache : redis >= 2.6.0

目前我的项目中简直都应用的是 graphql 接口, 采纳的 package 是 php lighthouse graphql, 那么次要的场景就是去统计好,graphql 接口的申请次数即可。

实现 GraphQL Record Middleware

首先建设一个 middleware 用于稍后记录接口的申请频率, 在这里能够应用 artisan 脚手架疾速创立:

php artisan make:middleware GraphQLRecord
<?php

namespace App\Http\Middleware;

use Closure;

class GraphQLRecord
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {return $next($request);
    }
}

而后增加到 app/config/lighthouse.php middleware 配置中, 或后增加到我的项目中 app/Http/Kernel.php 中, 设置为全局中间件

'middleware' => [
    \App\Http\Middleware\GraphQLRecord::class,
    \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
],

获取 GraphQL Operation Name

public function handle($request, Closure $next)
{$opName = $request->get('operationName');
        return $next($request);
}

获取到 Operation Name 之后, 开始就通过在 Redis 来实现一个接口计数器。

增加接口计数器

首先要设置咱们须要记录的工夫, 如 5 秒,60 秒, 半小时、一个小时、5 个小时、24 小时等, 用一个数组来实现,具体能够依据自我需要来调整。

const PRECISION = [5, 60, 1800, 3600, 86400];

而后就开始增加对接口计数的逻辑, 计数实现后, 咱们将其增加到 zsset 中, 不便后续进行数据查问等操作。

    /**
     * 更新申请计数器
     *
     * @param string $opName
     * @param integer $count
     * @return void
     */
    public function updateRequestCounter(string $opName, $count = 1)
    {$now   = microtime(true);
        $redis = self::getRedisConn();
        if ($redis) {$pipe = $redis->pipeline();
            foreach (self::PRECISION as $prec) {
                // 计算工夫片
                $pnow = intval($now / $prec) * $prec;
                // 生成一个 hash key 标识
                $hash = "request:counter:{$prec}:$opName";
                // 增长接口申请数
                $pipe->hincrby($hash, $pnow, 1);
                // 增加到汇合中, 不便后续数据查问
                $pipe->zadd('request:counter', [$hash => 0]);
            }
            $pipe->execute();}
    }

    /**
     * 获取 Redis 连贯
     *
     * @return object
     */
    public static function getRedisConn()
    {$redis = Redis::connection('cache');
        try {$redis->ping();
        } catch (Exception $ex) {
            $redis = null;
            // 丢给 sentry 报告
            app('sentry')->captureException($ex);
        }

        return $redis;
    }

而后申请一下接口, 用 medis 查看一下数据。

查问、剖析数据

数据记录欠缺后, 能够通过 opName 及 prec 两个属性来查问, 如查问 24 小时的 tag 接口拜访数据

    /**
     * 获取接口拜访计数
     *
     * @param string $opName
     * @param integer $prec
     * @return array
     */
    public static function getRequestCounter(string $opName, int $prec)
    {$data  = [];
        $redis = self::getRedisConn();
        if ($redis) {$hash     = "request:counter:{$prec}:$opName";
            $hashData = $redis->hgetall($hash);
            foreach ($hashData as $k => $v) {$date   = date("Y/m/d", $k);
                $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
            }
        }

        return $data;
    }

获取 tag 接口 24 小时的拜访统计

$data = $this->getRequestCounter('tagQuery', '86400');

革除数据

欠缺一系列步骤后, 咱们可能须要将过期和一些不必要的数据进行清理, 能够通过定时工作来进行定期清理, 相干实现如下:

/**
     * 清理申请计数
     *
     * @param integer $clearDay
     * @return void
     */
    public function clearRequestCounter($clearDay = 7)
    {
        $index     = 0;
        $startTime = microtime(true);
        $redis     = self::getRedisConn();
        if ($redis) {
            // 能够清理的状况下
            while ($index < $redis->zcard('request:counter')) {$hash = $redis->zrange('request:counter', $index, $index);
                $index++;

                // 以后 hash 存在
                if ($hash) {$hash = $hash[0];
                    // 计算删除截止工夫
                    $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

                    // 优先删除工夫较远的数据
                    $samples = array_map('intval', $redis->hkeys($hash));
                    sort($samples);

                    // 须要删除的数据
                    $removes = array_filter($samples, function ($item) use (&$cutoff) {return $item <= $cutoff;});
                    if (count($removes)) {$redis->hdel($hash, ...$removes);
                        // 如果整个数据都过期了的话, 就革除掉统计的数据
                        if (count($removes) == count($samples)) {$trans = $redis->transaction(['cas' => true]);
                            try {$trans->watch($hash);
                                if (!$trans->hlen($hash)) {$trans->multi();
                                    $trans->zrem('request:counter', $hash);
                                    $trans->execute();
                                    $index--;
                                } else {$trans->unwatch();
                                }
                            } catch (\Exception $ex) {dump($ex);
                            }
                        }
                    }

                }
            }
            dump('清理实现');
        }

    }

清理一个 30 天前的数据:

$this->clearRequestCounter(30);

整合代码

咱们将所有操作接口统计的代码, 独自封装到一个类中, 而后对外提供动态函数调用, 既实现了职责繁多, 又不便集成到其余不同的模块应用。

<?php
namespace App\Helpers;

use Illuminate\Support\Facades\Redis;

class RequestCounter
{const PRECISION = [5, 60, 1800, 3600, 86400];

    const REQUEST_COUNTER_CACHE_KEY = 'request:counter';

    /**
     * 更新申请计数器
     *
     * @param string $opName
     * @param integer $count
     * @return void
     */
    public static function updateRequestCounter(string $opName, $count = 1)
    {$now   = microtime(true);
        $redis = self::getRedisConn();
        if ($redis) {$pipe = $redis->pipeline();
            foreach (self::PRECISION as $prec) {
                // 计算工夫片
                $pnow = intval($now / $prec) * $prec;
                // 生成一个 hash key 标识
                $hash = self::counterCacheKey($opName, $prec);
                // 增长接口申请数
                $pipe->hincrby($hash, $pnow, 1);
                // 增加到汇合中, 不便后续数据查问
                $pipe->zadd(self::REQUEST_COUNTER_CACHE_KEY, [$hash => 0]);
            }
            $pipe->execute();}
    }

    /**
     * 获取 Redis 连贯
     *
     * @return object
     */
    public static function getRedisConn()
    {$redis = Redis::connection('cache');
        try {$redis->ping();
        } catch (Exception $ex) {
            $redis = null;
            // 丢给 sentry 报告
            app('sentry')->captureException($ex);
        }

        return $redis;
    }

    /**
     * 获取接口拜访计数
     *
     * @param string $opName
     * @param integer $prec
     * @return array
     */
    public static function getRequestCounter(string $opName, int $prec)
    {$data  = [];
        $redis = self::getRedisConn();
        if ($redis) {$hash     = self::counterCacheKey($opName, $prec);
            $hashData = $redis->hgetall($hash);
            foreach ($hashData as $k => $v) {$date   = date("Y/m/d", $k);
                $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
            }
        }

        return $data;
    }

    /**
     * 清理申请计数
     *
     * @param integer $clearDay
     * @return void
     */
    public static function clearRequestCounter($clearDay = 7)
    {
        $index     = 0;
        $startTime = microtime(true);
        $redis     = self::getRedisConn();
        if ($redis) {
            // 能够清理的状况下
            while ($index < $redis->zcard(self::REQUEST_COUNTER_CACHE_KEY)) {$hash = $redis->zrange(self::REQUEST_COUNTER_CACHE_KEY, $index, $index);
                $index++;

                // 以后 hash 存在
                if ($hash) {$hash = $hash[0];
                    // 计算删除截止工夫
                    $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

                    // 优先删除工夫较远的数据
                    $samples = array_map('intval', $redis->hkeys($hash));
                    sort($samples);

                    // 须要删除的数据
                    $removes = array_filter($samples, function ($item) use (&$cutoff) {return $item <= $cutoff;});
                    if (count($removes)) {$redis->hdel($hash, ...$removes);
                        // 如果整个数据都过期了的话, 就革除掉统计的数据
                        if (count($removes) == count($samples)) {$trans = $redis->transaction(['cas' => true]);
                            try {$trans->watch($hash);
                                if (!$trans->hlen($hash)) {$trans->multi();
                                    $trans->zrem(self::REQUEST_COUNTER_CACHE_KEY, $hash);
                                    $trans->execute();
                                    $index--;
                                } else {$trans->unwatch();
                                }
                            } catch (\Exception $ex) {dump($ex);
                            }
                        }
                    }

                }
            }
            dump('清理实现');
        }

    }

    public static function counterCacheKey($opName, $prec)
    {$key = "request:counter:{$prec}:$opName";

        return $key;
    }
}

在 Middleware 中应用.

<?php

namespace App\Http\Middleware;

use App\Helpers\RequestCounter;
use Closure;

class GraphQLRecord
{

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {$opName = $request->get('operationName');
        if (!empty($opName)) {RequestCounter::updateRequestCounter($opName);
        }

        return $next($request);
    }
}

结尾

上诉代码就实现了基于 GraphQL 的申请频率记录, 然而应用不止实用于 GraphQL 接口, 也能够基于 Rest 接口、模块计数等统计行为, 只有有惟一的 operation name 即可。

正文完
 0