Merge branch 'statsd' into develop

This commit is contained in:
ErickSkrauch 2017-11-22 22:49:45 +03:00
commit 5040c497ad
29 changed files with 265 additions and 19 deletions

View File

@ -35,6 +35,12 @@ RABBITMQ_USER=ely-accounts-app
RABBITMQ_PASS=ely-accounts-app-password
RABBITMQ_VHOST=/ely.by
## Параметры Statsd
STATSD_HOST=statsd.ely.by
STATSD_PORT=8125
# This value can be blank
STATSD_NAMESPACE=
## Конфигурация для Dev.
XDEBUG_CONFIG=remote_host=10.254.254.254
PHP_IDE_CONFIG=serverName=docker

View File

@ -9,6 +9,7 @@ class AspectKernel extends BaseAspectKernel {
protected function configureAop(AspectContainer $container): void {
$container->registerAspect(new aspects\MockDataAspect());
$container->registerAspect(new aspects\CollectMetricsAspect());
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace api\aop\annotations;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target("METHOD")
*/
class CollectModelMetrics {
/**
* @Required()
* @var string задаёт префикс для отправки метрик. Задаётся без ведущей и без завершающей точки.
*/
public $prefix = '';
}

View File

@ -0,0 +1,41 @@
<?php
namespace api\aop\aspects;
use api\aop\annotations\CollectModelMetrics;
use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Around;
use Yii;
class CollectMetricsAspect implements Aspect {
/**
* @param MethodInvocation $invocation Invocation
* @Around("@execution(api\aop\annotations\CollectModelMetrics)")
*/
public function sendMetrics(MethodInvocation $invocation) {
/** @var CollectModelMetrics $annotation */
$annotation = $invocation->getMethod()->getAnnotation(CollectModelMetrics::class);
$prefix = trim($annotation->prefix, '.');
Yii::$app->statsd->inc($prefix . '.attempt');
$result = $invocation->proceed();
if ($result !== false) {
Yii::$app->statsd->inc($prefix . '.success');
return $result;
}
/** @var \yii\base\Model $model */
$model = $invocation->getThis();
$errors = array_values($model->getFirstErrors());
if (!isset($errors[0])) {
Yii::error('Unsuccess result with empty errors list');
return false;
}
Yii::$app->statsd->inc($prefix . '.' . $errors[0]);
return false;
}
}

View File

@ -87,6 +87,7 @@ class OauthProcess {
*/
public function complete(): array {
try {
Yii::$app->statsd->inc('oauth.complete.attempt');
$grant = $this->getAuthorizationCodeGrant();
$authParams = $grant->checkAuthorizeParams();
$account = Yii::$app->user->identity->getAccount();
@ -94,6 +95,7 @@ class OauthProcess {
$clientModel = OauthClient::findOne($authParams->getClient()->getId());
if (!$this->canAutoApprove($account, $clientModel, $authParams)) {
Yii::$app->statsd->inc('oauth.complete.approve_required');
$isAccept = Yii::$app->request->post('accept');
if ($isAccept === null) {
throw new AcceptRequiredException();
@ -109,7 +111,12 @@ class OauthProcess {
'success' => true,
'redirectUri' => $redirectUri,
];
Yii::$app->statsd->inc('oauth.complete.success');
} catch (OAuthException $e) {
if (!$e instanceof AcceptRequiredException) {
Yii::$app->statsd->inc('oauth.complete.fail');
}
$response = $this->buildErrorResponse($e);
}
@ -139,8 +146,11 @@ class OauthProcess {
*/
public function getToken(): array {
try {
Yii::$app->statsd->inc('oauth.issueToken.attempt');
$response = $this->server->issueAccessToken();
Yii::$app->statsd->inc('oauth.issueToken.success');
} catch (OAuthException $e) {
Yii::$app->statsd->inc('oauth.issueToken.fail');
Yii::$app->response->statusCode = $e->httpStatusCode;
$response = [
'error' => $e->errorType,

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm;
use api\modules\accounts\models\ChangeUsernameForm;
use api\validators\EmailActivationKeyValidator;
@ -20,6 +21,7 @@ class ConfirmEmailForm extends ApiForm {
}
/**
* @CollectModelMetrics(prefix="signup.confirmEmail")
* @return \api\components\User\AuthenticationResult|bool
* @throws ErrorException
*/

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
use api\models\base\ApiForm;
use common\emails\EmailHelper;
@ -55,6 +56,11 @@ class ForgotPasswordForm extends ApiForm {
}
}
/**
* @CollectModelMetrics(prefix="authentication.forgotPassword")
* @return bool
* @throws ErrorException
*/
public function forgotPassword() {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm;
use api\validators\TotpValidator;
use common\helpers\Error as E;
@ -87,6 +88,7 @@ class LoginForm extends ApiForm {
}
/**
* @CollectModelMetrics(prefix="authentication.login")
* @return \api\components\User\AuthenticationResult|bool
*/
public function login() {

View File

@ -1,12 +1,18 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm;
use Yii;
class LogoutForm extends ApiForm {
/**
* @CollectModelMetrics(prefix="authentication.logout")
* @return bool
*/
public function logout() : bool {
$component = \Yii::$app->user;
$component = Yii::$app->user;
$session = $component->getActiveSession();
if ($session === null) {
return true;

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm;
use api\validators\EmailActivationKeyValidator;
use common\helpers\Error as E;
@ -36,6 +37,7 @@ class RecoverPasswordForm extends ApiForm {
}
/**
* @CollectModelMetrics(prefix="authentication.recoverPassword")
* @return \api\components\User\AuthenticationResult|bool
* @throws ErrorException
*/

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\models\base\ApiForm;
use common\helpers\Error as E;
use common\models\AccountSession;
@ -32,6 +33,7 @@ class RefreshTokenForm extends ApiForm {
}
/**
* @CollectModelMetrics(prefix="authentication.renew")
* @return \api\components\User\AuthenticationResult|bool
*/
public function renew() {

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
use common\emails\EmailHelper;
use api\models\base\ApiForm;
@ -63,6 +64,7 @@ class RegistrationForm extends ApiForm {
}
/**
* @CollectModelMetrics(prefix="signup.register")
* @return Account|null the saved model or null if saving fails
* @throws Exception
*/

View File

@ -1,6 +1,7 @@
<?php
namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics;
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
use common\emails\EmailHelper;
use api\models\base\ApiForm;
@ -53,6 +54,11 @@ class RepeatAccountActivationForm extends ApiForm {
}
}
/**
* @CollectModelMetrics(prefix="signup.repeatEmail")
* @return bool
* @throws ErrorException
*/
public function sendRepeatMessage() {
if (!$this->validate()) {
return false;

View File

@ -1,11 +1,15 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use yii\base\ErrorException;
use const \common\LATEST_RULES_VERSION;
class AcceptRulesForm extends AccountActionForm {
/**
* @CollectModelMetrics(prefix="accounts.acceptRules")
*/
public function performAction(): bool {
$account = $this->getAccount();
$account->rules_agreement_version = LATEST_RULES_VERSION;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\validators\EmailActivationKeyValidator;
use common\helpers\Amqp;
use common\models\amqp\EmailChanged;
@ -19,6 +20,9 @@ class ChangeEmailForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.changeEmail")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use common\validators\LanguageValidator;
@ -15,6 +16,9 @@ class ChangeLanguageForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.switchLanguage")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\components\User\Component;
use api\exceptions\ThisShouldNotHappenException;
use api\validators\PasswordRequiredValidator;
@ -43,6 +44,9 @@ class ChangePasswordForm extends AccountActionForm {
}
}
/**
* @CollectModelMetrics(prefix="accounts.changePassword")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use api\validators\PasswordRequiredValidator;
use common\helpers\Amqp;
@ -26,6 +27,9 @@ class ChangeUsernameForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.changeUsername")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use api\validators\PasswordRequiredValidator;
use api\validators\TotpValidator;
@ -21,6 +22,9 @@ class DisableTwoFactorAuthForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.disableTwoFactorAuth")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\components\User\Component;
use api\exceptions\ThisShouldNotHappenException;
use api\validators\PasswordRequiredValidator;
@ -23,6 +24,9 @@ class EnableTwoFactorAuthForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.enableTwoFactorAuth")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use common\emails\EmailHelper;
use api\validators\PasswordRequiredValidator;
@ -34,6 +35,9 @@ class SendEmailVerificationForm extends AccountActionForm {
}
}
/**
* @CollectModelMetrics(prefix="accounts.sendEmailVerification")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -1,6 +1,7 @@
<?php
namespace api\modules\accounts\models;
use api\aop\annotations\CollectModelMetrics;
use api\exceptions\ThisShouldNotHappenException;
use common\emails\EmailHelper;
use api\validators\EmailActivationKeyValidator;
@ -22,6 +23,9 @@ class SendNewEmailVerificationForm extends AccountActionForm {
];
}
/**
* @CollectModelMetrics(prefix="accounts.sendNewEmailVerification")
*/
public function performAction(): bool {
if (!$this->validate()) {
return false;

View File

@ -6,6 +6,7 @@ use api\modules\session\exceptions\IllegalArgumentException;
use api\modules\session\models\protocols\HasJoinedInterface;
use api\modules\session\Module as Session;
use common\models\Account;
use Yii;
use yii\base\ErrorException;
use yii\base\Model;
@ -19,6 +20,7 @@ class HasJoinedForm extends Model {
}
public function hasJoined(): Account {
Yii::$app->statsd->inc('sessionserver.hasJoined.attempt');
if (!$this->protocol->validate()) {
throw new IllegalArgumentException();
}
@ -26,13 +28,12 @@ class HasJoinedForm extends Model {
$serverId = $this->protocol->getServerId();
$username = $this->protocol->getUsername();
Session::info(
"Server with server_id = '{$serverId}' trying to verify has joined user with username = '{$username}'."
);
Session::info("Server with server_id = '{$serverId}' trying to verify has joined user with username = '{$username}'.");
$joinModel = SessionModel::find($username, $serverId);
if ($joinModel === null) {
Session::error("Not found join operation for username = '{$username}'.");
Yii::$app->statsd->inc('sessionserver.hasJoined.fail_no_join');
throw new ForbiddenOperationException('Invalid token.');
}
@ -42,9 +43,8 @@ class HasJoinedForm extends Model {
throw new ErrorException('Account must exists');
}
Session::info(
"User with username = '{$username}' successfully verified by server with server_id = '{$serverId}'."
);
Session::info("User with username = '{$username}' successfully verified by server with server_id = '{$serverId}'.");
Yii::$app->statsd->inc('sessionserver.hasJoined.success');
return $account;
}

View File

@ -53,6 +53,7 @@ class JoinForm extends Model {
$serverId = $this->serverId;
$accessToken = $this->accessToken;
Session::info("User with access_token = '{$accessToken}' trying join to server with server_id = '{$serverId}'.");
Yii::$app->statsd->inc('sessionserver.join.attempts');
if (!$this->validate()) {
return false;
}
@ -63,10 +64,8 @@ class JoinForm extends Model {
throw new ErrorException('Cannot save join session model');
}
Session::info(
"User with access_token = '{$accessToken}' and nickname = '{$account->username}' successfully joined to " .
"server_id = '{$serverId}'."
);
Session::info("User with access_token = '{$accessToken}' and nickname = '{$account->username}' successfully joined to server_id = '{$serverId}'.");
Yii::$app->statsd->inc('sessionserver.join.success');
return true;
}
@ -97,9 +96,11 @@ class JoinForm extends Model {
/** @var MinecraftAccessKey|null $accessModel */
$accessModel = MinecraftAccessKey::findOne($accessToken);
if ($accessModel !== null) {
Yii::$app->statsd->inc('sessionserver.authentication.legacy_minecraft_protocol');
/** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
if ($accessModel->isExpired()) {
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
Yii::$app->statsd->inc('sessionserver.authentication.legacy_minecraft_protocol_token_expired');
throw new ForbiddenOperationException('Expired access_token.');
}
@ -113,11 +114,14 @@ class JoinForm extends Model {
if ($identity === null) {
Session::error("User with access_token = '{$accessToken}' failed join by wrong access_token.");
Yii::$app->statsd->inc('sessionserver.join.fail_wrong_token');
throw new ForbiddenOperationException('Invalid access_token.');
}
Yii::$app->statsd->inc('sessionserver.authentication.oauth2');
if (!Yii::$app->user->can(P::MINECRAFT_SERVER_SESSION)) {
Session::error("User with access_token = '{$accessToken}' doesn't have enough scopes to make join.");
Yii::$app->statsd->inc('sessionserver.authentication.oauth2_not_enough_scopes');
throw new ForbiddenOperationException('The token does not have required scope.');
}
@ -127,18 +131,14 @@ class JoinForm extends Model {
$selectedProfile = $this->selectedProfile;
$isUuid = StringHelper::isUuid($selectedProfile);
if ($isUuid && $account->uuid !== $this->normalizeUUID($selectedProfile)) {
Session::error(
"User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}'," .
" but access_token issued to account with id = '{$account->uuid}'."
);
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with id = '{$account->uuid}'.");
Yii::$app->statsd->inc('sessionserver.join.fail_uuid_mismatch');
throw new ForbiddenOperationException('Wrong selected_profile.');
}
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower($selectedProfile)) {
Session::error(
"User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}'," .
" but access_token issued to account with username = '{$account->username}'."
);
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with username = '{$account->username}'.");
Yii::$app->statsd->inc('sessionserver.join.fail_username_mismatch');
throw new ForbiddenOperationException('Invalid credentials');
}

View File

@ -4,6 +4,8 @@ use api\aop\AspectKernel;
use common\config\ConfigLoader;
use yii\web\Application;
$time = microtime(true);
require __DIR__ . '/../../vendor/autoload.php';
defined('YII_DEBUG') or define('YII_DEBUG', in_array(getenv('YII_DEBUG'), ['true', '1']));
@ -29,3 +31,7 @@ $config = ConfigLoader::load('api');
$application = new Application($config);
$application->run();
$timeDifference = (microtime(true) - $time) * 1000;
fastcgi_finish_request();
Yii::$app->statsd->time('request.time', $timeDifference);

View File

@ -24,6 +24,7 @@ class Yii extends \yii\BaseYii {
* @property \common\components\EmailRenderer $emailRenderer
* @property \mito\sentry\Component $sentry
* @property \api\components\OAuth2\Component $oauth
* @property \common\components\StatsD $statsd
*/
abstract class BaseApplication extends yii\base\Application {
}

View File

@ -0,0 +1,89 @@
<?php
namespace common\components;
use Domnikl\Statsd\Client;
use Domnikl\Statsd\Connection;
use yii\base\Component;
class StatsD extends Component {
/**
* @var string
*/
public $host;
/**
* @var int
*/
public $port = 8125;
/**
* @var string
*/
public $namespace = '';
private $client;
public function inc(string $key): void {
$this->getClient()->increment($key);
}
public function dec(string $key): void {
$this->getClient()->decrement($key);
}
public function count(string $key, int $value): void {
$this->getClient()->count($key, $value);
}
public function time(string $key, float $time): void {
$this->getClient()->timing($key, floor($time));
}
public function startTiming(string $key): void {
$this->getClient()->startTiming($key);
}
public function endTiming(string $key): void {
$this->getClient()->endTiming($key);
}
public function peakMemoryUsage(string $key): void {
$this->getClient()->memory($key);
}
/**
* Pass delta values as a string.
* Accepts both positive (+11) and negative (-4) delta values.
* $statsd->gauge('foobar', 3);
* $statsd->gauge('foobar', '+11');
*
* @param string $key
* @param string|int $value
*/
public function gauge(string $key, $value): void {
$this->getClient()->gauge($key, $value);
}
public function set(string $key, int $value): void {
$this->getClient()->set($key, $value);
}
public function getClient(): Client {
if ($this->client === null) {
$connection = $this->createConnection();
$this->client = new Client($connection, $this->namespace);
}
return $this->client;
}
protected function createConnection(): Connection {
if (!empty($this->host) && !empty($this->port)) {
return new Connection\UdpSocket($this->host, $this->port);
}
return new Connection\Blackhole();
}
}

View File

@ -90,6 +90,12 @@ return [
'itemFile' => '@common/rbac/.generated/items.php',
'ruleFile' => '@common/rbac/.generated/rules.php',
],
'statsd' => [
'class' => common\components\StatsD::class,
'host' => getenv('STATSD_HOST'),
'port' => getenv('STATSD_PORT') ?: 8125,
'namespace' => getenv('STATSD_NAMESPACE') ?: 'ely.accounts.' . gethostname() . '.app',
],
],
'container' => [
'definitions' => [

View File

@ -32,11 +32,14 @@ class AccountQueueController extends AmqpController {
}
public function routeUsernameChanged(UsernameChanged $body): bool {
Yii::$app->statsd->inc('worker.account.usernameChanged.attempt');
$mojangApi = $this->createMojangApi();
try {
$response = $mojangApi->usernameToUUID($body->newUsername);
Yii::$app->statsd->inc('worker.account.usernameChanged.found');
} catch (NoContentException $e) {
$response = false;
Yii::$app->statsd->inc('worker.account.usernameChanged.not_found');
} catch (RequestException $e) {
return true;
}