diff --git a/.env b/.env
index 9f6050fe50..86d7c558c4 100644
--- a/.env
+++ b/.env
@@ -43,3 +43,5 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
_APP_USAGE_STATS=enabled
+_APP_LOGGING_PROVIDER=
+_APP_LOGGING_CONFIG=
diff --git a/.travis.yml_tmp b/.travis.yml_tmp
index e433217a96..197f30923a 100644
--- a/.travis.yml_tmp
+++ b/.travis.yml_tmp
@@ -65,6 +65,7 @@ script:
exit 1
fi
- docker-compose logs appwrite
+- docker-compose logs appwrite-realtime
- docker-compose logs mariadb
- docker-compose logs appwrite-worker-functions
- docker-compose exec appwrite doctor
diff --git a/CHANGES.md b/CHANGES.md
index 536eb247b2..d724ab0d06 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -24,6 +24,8 @@
- Deno 1.12
- Deno 1.13
- Deno 1.14
+ - PHP 8.1
+ - Node 17
- Added translations:
- German `de` by @SoftCreatR in https://github.com/appwrite/appwrite/pull/1790
- Hebrew `he` by @Kokoden in https://github.com/appwrite/appwrite/pull/1846
diff --git a/Dockerfile b/Dockerfile
index 8af07f2e93..fe515f7b53 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -181,7 +181,9 @@ ENV _APP_SERVER=swoole \
_APP_MAINTENANCE_RETENTION_AUDIT=1209600 \
# 1 Day = 86400 s
_APP_MAINTENANCE_RETENTION_ABUSE=86400 \
- _APP_MAINTENANCE_INTERVAL=86400
+ _APP_MAINTENANCE_INTERVAL=86400 \
+ _APP_LOGGING_PROVIDER= \
+ _APP_LOGGING_CONFIG=
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
diff --git a/app/config/variables.php b/app/config/variables.php
index 5e5e813065..43687110ae 100644
--- a/app/config/variables.php
+++ b/app/config/variables.php
@@ -150,6 +150,24 @@ return [
'question' => '',
'filter' => ''
],
+ [
+ 'name' => '_APP_LOGGING_PROVIDER',
+ 'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\'',
+ 'introduction' => '0.12.0',
+ 'default' => '',
+ 'required' => false,
+ 'question' => '',
+ 'filter' => ''
+ ],
+ [
+ 'name' => '_APP_LOGGING_CONFIG',
+ 'description' => 'This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be \'SENTRY_API_KEY;SENTRY_APP_ID\'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key.',
+ 'introduction' => '0.12.0',
+ 'default' => '',
+ 'required' => false,
+ 'question' => '',
+ 'filter' => ''
+ ],
[
'name' => '_APP_USAGE_AGGREGATION_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds.',
diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php
index 2841c6fa48..4e5020d119 100644
--- a/app/controllers/api/database.php
+++ b/app/controllers/api/database.php
@@ -973,10 +973,12 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
throw new Exception($validator->getDescription(), 400);
}
+ $size = $max > 2147483647 ? 8 : 4; // Automatically create BigInt depending on max value
+
$attribute = createAttribute($collectionId, new Document([
'key' => $key,
'type' => Database::VAR_INTEGER,
- 'size' => 0,
+ 'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
@@ -1679,7 +1681,7 @@ App::get('/v1/database/collections/:collectionId/documents')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST)
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
- ->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings.', true) // TODO: research limitations - temporarily unlimited length
+ ->param('queries', [], new ArrayList(new Text(0), 100), 'Array of query strings.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the document used as the starting point for the query, excluding the document itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
@@ -1717,7 +1719,15 @@ App::get('/v1/database/collections/:collectionId/documents')
}
}
- $queries = \array_map(fn ($query) => Query::parse($query), $queries);
+ $queries = \array_map(function ($query) {
+ $query = Query::parse($query);
+
+ if (\count($query->getValues()) > 100) {
+ throw new Exception("You cannot use more than 100 query values on attribute '{$query->getAttribute()}'", 400);
+ }
+
+ return $query;
+ }, $queries);
if (!empty($queries)) {
$validator = new QueriesValidator(new QueryValidator($collection->getAttribute('attributes', [])), $collection->getAttribute('indexes', []), true);
diff --git a/app/controllers/general.php b/app/controllers/general.php
index f5d3b5b676..27ee03d7a9 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -3,6 +3,8 @@
require_once __DIR__.'/../init.php';
use Utopia\App;
+use Utopia\Logger\Log;
+use Utopia\Logger\Log\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\View;
@@ -297,19 +299,72 @@ App::options(function ($request, $response) {
->noContent();
}, ['request', 'response']);
-App::error(function ($error, $utopia, $request, $response, $layout, $project) {
+App::error(function ($error, $utopia, $request, $response, $layout, $project, $logger, $loggerBreadcrumbs) {
/** @var Exception $error */
/** @var Utopia\App $utopia */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
/** @var Utopia\Database\Document $project */
+ /** @var Utopia\Logger\Logger $logger */
+ /** @var Utopia\Logger\Log\Breadcrumb[] $loggerBreadcrumbs */
+
+ $version = App::getEnv('_APP_VERSION', 'UNKNOWN');
+ $route = $utopia->match($request);
+
+ if($logger) {
+ if($error->getCode() >= 500 || $error->getCode() === 0) {
+ try {
+ $user = $utopia->getResource('user');
+ /** @var Appwrite\Database\Document $user */
+ } catch(\Throwable $th) {
+ // All good, user is optional information for logger
+ }
+
+ $log = new Utopia\Logger\Log();
+
+ if(isset($user) && !$user->isEmpty()) {
+ $log->setUser(new User($user->getId()));
+ }
+
+ $log->setNamespace("http");
+ $log->setServer(\gethostname());
+ $log->setVersion($version);
+ $log->setType(Log::TYPE_ERROR);
+ $log->setMessage($error->getMessage());
+
+ $log->addTag('method', $route->getMethod());
+ $log->addTag('url', $route->getPath());
+ $log->addTag('verboseType', get_class($error));
+ $log->addTag('code', $error->getCode());
+ $log->addTag('projectId', $project->getId());
+ $log->addTag('hostname', $request->getHostname());
+ $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
+
+ $log->addExtra('file', $error->getFile());
+ $log->addExtra('line', $error->getLine());
+ $log->addExtra('trace', $error->getTraceAsString());
+ $log->addExtra('roles', Authorization::$roles);
+
+ $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
+ $log->setAction($action);
+
+ $isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
+ $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
+
+ foreach($loggerBreadcrumbs as $loggerBreadcrumb) {
+ $log->addBreadcrumb($loggerBreadcrumb);
+ }
+
+ $responseCode = $logger->addLog($log);
+ Console::info('Log pushed with status code: '.$responseCode);
+ }
+ }
if ($error instanceof PDOException) {
throw $error;
}
- $route = $utopia->match($request);
$template = ($route) ? $route->getLabel('error', null) : null;
if (php_sapi_name() === 'cli') {
@@ -326,8 +381,6 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) {
Console::error('[Error] Line: '.$error->getLine());
}
- $version = App::getEnv('_APP_VERSION', 'UNKNOWN');
-
switch ($error->getCode()) { // Don't show 500 errors!
case 400: // Error allowed publicly
case 401: // Error allowed publicly
@@ -394,7 +447,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) {
$response->dynamic(new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR);
-}, ['error', 'utopia', 'request', 'response', 'layout', 'project']);
+}, ['error', 'utopia', 'request', 'response', 'layout', 'project', 'logger', 'loggerBreadcrumbs']);
App::get('/manifest.json')
->desc('Progressive app manifest file')
diff --git a/app/http.php b/app/http.php
index e7dd0f1ec6..7df9b963ea 100644
--- a/app/http.php
+++ b/app/http.php
@@ -17,6 +17,8 @@ use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Database\Document;
use Utopia\Swoole\Files;
use Appwrite\Utopia\Request;
+use Utopia\Logger\Log;
+use Utopia\Logger\Log\User;
$http = new Server("0.0.0.0", App::getEnv('PORT', 80));
@@ -207,6 +209,59 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$app->run($request, $response);
} catch (\Throwable $th) {
+ $version = App::getEnv('_APP_VERSION', 'UNKNOWN');
+
+ $logger = $app->getResource("logger");
+ if($logger) {
+ try {
+ $user = $app->getResource('user');
+ /** @var Appwrite\Database\Document $user */
+ } catch(\Throwable $_th) {
+ // All good, user is optional information for logger
+ }
+
+ $loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs");
+ $route = $app->match($request);
+
+ $log = new Utopia\Logger\Log();
+
+ if(isset($user) && !$user->isEmpty()) {
+ $log->setUser(new User($user->getId()));
+ }
+
+ $log->setNamespace("http");
+ $log->setServer(\gethostname());
+ $log->setVersion($version);
+ $log->setType(Log::TYPE_ERROR);
+ $log->setMessage($th->getMessage());
+
+ $log->addTag('method', $route->getMethod());
+ $log->addTag('url', $route->getPath());
+ $log->addTag('verboseType', get_class($th));
+ $log->addTag('code', $th->getCode());
+ // $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant
+ $log->addTag('hostname', $request->getHostname());
+ $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
+
+ $log->addExtra('file', $th->getFile());
+ $log->addExtra('line', $th->getLine());
+ $log->addExtra('trace', $th->getTraceAsString());
+ $log->addExtra('roles', Authorization::$roles);
+
+ $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
+ $log->setAction($action);
+
+ $isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
+ $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
+
+ foreach($loggerBreadcrumbs as $loggerBreadcrumb) {
+ $log->addBreadcrumb($loggerBreadcrumb);
+ }
+
+ $responseCode = $logger->addLog($log);
+ Console::info('Log pushed with status code: '.$responseCode);
+ }
+
Console::error('[Error] Type: '.get_class($th));
Console::error('[Error] Message: '.$th->getMessage());
Console::error('[Error] File: '.$th->getFile());
@@ -221,12 +276,20 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$swooleResponse->setStatusCode(500);
- if(App::isDevelopment()) {
- $swooleResponse->end('error: '.$th->getMessage());
- }
- else {
- $swooleResponse->end('500: Server Error');
- }
+ $output = ((App::isDevelopment())) ? [
+ 'message' => 'Error: '. $th->getMessage(),
+ 'code' => 500,
+ 'file' => $th->getFile(),
+ 'line' => $th->getLine(),
+ 'trace' => $th->getTrace(),
+ 'version' => $version,
+ ] : [
+ 'message' => 'Error: Server Error',
+ 'code' => 500,
+ 'version' => $version,
+ ];
+
+ $swooleResponse->end(\json_encode($output));
} finally {
/** @var PDOPool $dbPool */
$dbPool = $register->get('dbPool');
diff --git a/app/init.php b/app/init.php
index 3f74f34c01..89f2361d2b 100644
--- a/app/init.php
+++ b/app/init.php
@@ -30,6 +30,7 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\View;
use Utopia\App;
+use Utopia\Logger\Logger;
use Utopia\Config\Config;
use Utopia\Locale\Locale;
use Utopia\Registry\Registry;
@@ -144,7 +145,7 @@ Config::load('locale-continents', __DIR__.'/config/locale/continents.php');
Config::load('storage-logos', __DIR__.'/config/storage/logos.php');
Config::load('storage-mimes', __DIR__.'/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__.'/config/storage/inputs.php');
-Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php');
+Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php');
$user = App::getEnv('_APP_REDIS_USER','');
$pass = App::getEnv('_APP_REDIS_PASS','');
@@ -375,6 +376,22 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) {
/*
* Registry
*/
+$register->set('logger', function () { // Register error logger
+ $providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
+ $providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
+
+ if(empty($providerName) || empty($providerConfig)) {
+ return null;
+ }
+
+ if(!Logger::hasProvider($providerName)) {
+ throw new Exception("Logging provider not supported. Logging disabled.");
+ }
+
+ $classname = '\\Utopia\\Logger\\Adapter\\'.\ucfirst($providerName);
+ $adapter = new $classname($providerConfig);
+ return new Logger($adapter);
+});
$register->set('dbPool', function () { // Register DB connection
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
@@ -581,6 +598,14 @@ Locale::setLanguageFromJSON('zh-tw', __DIR__.'/config/locale/translations/zh-tw.
]);
// Runtime Execution
+App::setResource('logger', function($register) {
+ return $register->get('logger');
+}, ['register']);
+
+App::setResource('loggerBreadcrumbs', function() {
+ return [];
+});
+
App::setResource('register', fn() => $register);
App::setResource('layout', function($locale) {
diff --git a/app/realtime.php b/app/realtime.php
index f3361ab89e..6db9ce718a 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -14,6 +14,8 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\Console;
+use Utopia\Config\Config;
+use Utopia\Logger\Log;
use Utopia\Database\Database;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
@@ -51,6 +53,43 @@ $adapter->setPackageMaxLength(64000); // Default maximum Package Size (64kb)
$server = new Server($adapter);
+$logError = function(Throwable $error, string $action) use ($register) {
+ $logger = $register->get('logger');
+
+ if($logger) {
+ $version = App::getEnv('_APP_VERSION', 'UNKNOWN');
+
+ $log = new Log();
+ $log->setNamespace("realtime");
+ $log->setServer(\gethostname());
+ $log->setVersion($version);
+ $log->setType(Log::TYPE_ERROR);
+ $log->setMessage($error->getMessage());
+
+ $log->addTag('code', $error->getCode());
+ $log->addTag('verboseType', get_class($error));
+
+ $log->addExtra('file', $error->getFile());
+ $log->addExtra('line', $error->getLine());
+ $log->addExtra('trace', $error->getTraceAsString());
+
+ $log->setAction($action);
+
+ $isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
+ $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
+
+ $responseCode = $logger->addLog($log);
+ Console::info('Realtime log pushed with status code: '.$responseCode);
+ }
+
+ Console::error('[Error] Type: ' . get_class($error));
+ Console::error('[Error] Message: ' . $error->getMessage());
+ Console::error('[Error] File: ' . $error->getFile());
+ Console::error('[Error] Line: ' . $error->getLine());
+};
+
+$server->error($logError);
+
function getDatabase(Registry &$register, string $namespace)
{
$db = $register->get('dbPool')->get();
@@ -70,13 +109,13 @@ function getDatabase(Registry &$register, string $namespace)
];
};
-$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
+$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
Console::success('Server started succefully');
/**
* Create document for this worker to share stats across Containers.
*/
- go(function () use ($register, $containerId, &$statsDocument) {
+ go(function () use ($register, $containerId, &$statsDocument, $logError) {
try {
[$database, $returnDatabase] = getDatabase($register, '_project_console');
$document = new Document([
@@ -90,10 +129,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
]);
$statsDocument = Authorization::skip(fn() => $database->createDocument('realtime', $document));
} catch (\Throwable $th) {
- Console::error('[Error] Type: ' . get_class($th));
- Console::error('[Error] Message: ' . $th->getMessage());
- Console::error('[Error] File: ' . $th->getFile());
- Console::error('[Error] Line: ' . $th->getLine());
+ call_user_func($logError, $th, "createWorkerDocument");
} finally {
call_user_func($returnDatabase);
}
@@ -102,7 +138,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
/**
* Save current connections to the Database every 5 seconds.
*/
- Timer::tick(5000, function () use ($register, $stats, $containerId, &$statsDocument) {
+ Timer::tick(5000, function () use ($register, $stats, $containerId, &$statsDocument, $logError) {
/** @var Document $statsDocument */
foreach ($stats as $projectId => $value) {
$connections = $stats->get($projectId, 'connections') ?? 0;
@@ -142,23 +178,20 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
Authorization::skip(fn() => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (\Throwable $th) {
- Console::error('[Error] Type: ' . get_class($th));
- Console::error('[Error] Message: ' . $th->getMessage());
- Console::error('[Error] File: ' . $th->getFile());
- Console::error('[Error] Line: ' . $th->getLine());
+ call_user_func($logError, $th, "updateWorkerDocument");
} finally {
call_user_func($returnDatabase);
}
});
});
-$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
+$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started succefully');
$attempts = 0;
$start = time();
- Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
+ Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -300,6 +333,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
});
} catch (\Throwable $th) {
+ call_user_func($logError, $th, "pubSubConnection");
+
Console::error('Pub/sub error: ' . $th->getMessage());
$register->get('redisPool')->put($redis);
$attempts++;
@@ -312,7 +347,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
-$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
+$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) {
$app = new App('UTC');
$request = new Request($request);
$response = new Response(new SwooleResponse());
@@ -409,6 +444,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
} catch (\Throwable $th) {
+ call_user_func($logError, $th, "initServer");
+
$response = [
'type' => 'error',
'data' => [
diff --git a/app/tasks/doctor.php b/app/tasks/doctor.php
index 47e523ff94..20cafdf744 100644
--- a/app/tasks/doctor.php
+++ b/app/tasks/doctor.php
@@ -3,6 +3,7 @@
global $cli;
use Appwrite\ClamAV\Network;
+use Utopia\Logger\Logger;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\App;
@@ -82,6 +83,16 @@ $cli
Console::log('🟢 HTTPS force option is enabled');
}
+
+ $providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
+ $providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
+
+ if(empty($providerName) || empty($providerConfig) || !Logger::hasProvider($providerName)) {
+ Console::log('🔴 Logging adapter is disabled');
+ } else {
+ Console::log('🟢 Logging adapter is enabled (' . $providerName . ')');
+ }
+
\sleep(0.2);
try {
diff --git a/app/views/console/comps/header.phtml b/app/views/console/comps/header.phtml
index 6a258f60e2..5fd9d6d42a 100644
--- a/app/views/console/comps/header.phtml
+++ b/app/views/console/comps/header.phtml
@@ -211,7 +211,7 @@
required
maxlength="36"
class=""
- pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{1,36}$"
+ pattern="^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,36}$"
name="projectId" />
diff --git a/app/views/console/database/collection.phtml b/app/views/console/database/collection.phtml
index a9a3e692aa..34dc84e913 100644
--- a/app/views/console/database/collection.phtml
+++ b/app/views/console/database/collection.phtml
@@ -564,8 +564,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+ Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
@@ -641,8 +641,8 @@ $logs = $this->getParam('logs', null);
-
- Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+ Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -723,8 +723,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -804,8 +804,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -874,8 +874,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -947,8 +947,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -1017,8 +1017,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
Required
@@ -1087,8 +1087,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot
@@ -1184,8 +1184,8 @@ $logs = $this->getParam('logs', null);
-
-
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore
+
+
Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot